2946 lines
134 KiB
Bash
Executable File
2946 lines
134 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The production change that breaks these cases is accepting an unverified
|
|
# download, a wrong signer, or executable/malformed provenance data.
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
fixtures="$repo_dir/tests/setup/fixtures/provenance"
|
|
config="$repo_dir/setup/provenance/installers.conf"
|
|
provenance_readme="$repo_dir/setup/provenance/README.md"
|
|
test_tmp="$(mktemp -d)"
|
|
host_gnupg="${GNUPGHOME:-$HOME/.gnupg}"
|
|
host_rpmdb="/usr/lib/sysimage/rpm/rpmdb.sqlite"
|
|
ambient_gnupg="$test_tmp/fresh-ambient-gnupg"
|
|
|
|
cleanup() {
|
|
rm -rf -- "$test_tmp"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
fail() {
|
|
printf 'package provenance contract: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# The rotation ledger is part of the trust contract: it must preserve GPG's
|
|
# producer status, name every independent pin site, and claim only evidence
|
|
# retained from the one authorized Terra container.
|
|
if awk '/gpg/ && /[|]/ && /awk/ { found = 1 } END { exit !found }' \
|
|
"$provenance_readme"; then
|
|
fail 'provenance README pipes GPG into a parser and can lose producer status'
|
|
fi
|
|
for rotation_site in 'setup/provenance/keys/' 'setup/provenance/installers.conf' \
|
|
'_require_policy_value' 'tests/setup/package-provenance-contract' \
|
|
'setup/provenance/README.md'; do
|
|
grep -qF "$rotation_site" "$provenance_readme" \
|
|
|| fail "provenance rotation policy omits $rotation_site"
|
|
done
|
|
grep -qF 'same-container post-check independently confirmed' "$provenance_readme" \
|
|
&& fail 'Terra proof claims an unretained same-container post-check'
|
|
|
|
expect_success() {
|
|
"$@" || fail "expected success: $*"
|
|
}
|
|
|
|
expect_failure() {
|
|
if "$@"; then
|
|
fail "expected failure: $*"
|
|
fi
|
|
}
|
|
|
|
assert_file_bytes() {
|
|
local path="$1" expected="$2"
|
|
[[ "$(<"$path")" == "$expected" ]] || fail "unexpected bytes in $path"
|
|
}
|
|
|
|
snapshot() {
|
|
local path="$1"
|
|
if [[ -e "$path" ]]; then
|
|
stat -c '%i:%s:%Y:%Z' "$path"
|
|
else
|
|
printf 'absent'
|
|
fi
|
|
}
|
|
|
|
snapshot_gpg_state() {
|
|
local path="$1" file relative
|
|
[[ -d "$path" ]] || {
|
|
printf 'absent\n'
|
|
return 0
|
|
}
|
|
|
|
{
|
|
printf 'directory\0'
|
|
find "$path" -mindepth 1 -print0 | LC_ALL=C sort -z \
|
|
| while IFS= read -r -d '' file; do
|
|
relative="${file#"$path"/}"
|
|
printf '%s\0' "$relative"
|
|
if [[ -L "$file" ]]; then
|
|
printf 'symlink\0%s\0' "$(readlink -- "$file")"
|
|
elif [[ -f "$file" ]]; then
|
|
printf 'file\0%s\0' "$(sha256sum -- "$file" | awk '{ print $1 }')"
|
|
elif [[ -d "$file" ]]; then
|
|
printf 'directory\0'
|
|
else
|
|
printf 'other\0%s\0' "$(stat -c '%F:%a:%s:%Y:%Z' "$file")"
|
|
fi
|
|
done
|
|
} | sha256sum | awk '{ print $1 }'
|
|
}
|
|
|
|
snapshot_file_state() {
|
|
local path="$1"
|
|
if [[ -f "$path" ]]; then
|
|
printf 'file:%s:%s\n' "$(stat -c '%a:%s:%Y:%Z' "$path")" \
|
|
"$(sha256sum "$path" | awk '{ print $1 }')"
|
|
elif [[ -L "$path" ]]; then
|
|
printf 'symlink:%s\n' "$(readlink -- "$path")"
|
|
else
|
|
printf 'absent\n'
|
|
fi
|
|
}
|
|
|
|
export GNUPGHOME="$ambient_gnupg"
|
|
mkdir -m 700 "$ambient_gnupg"
|
|
printf 'baseline-value\n' > "$ambient_gnupg/unexpected-entry"
|
|
ambient_before="$(snapshot "$ambient_gnupg")"
|
|
ambient_gpg_files_before="$(snapshot_gpg_state "$ambient_gnupg")"
|
|
printf 'tampered-value\n' > "$ambient_gnupg/unexpected-entry"
|
|
[[ "$ambient_gpg_files_before" != "$(snapshot_gpg_state "$ambient_gnupg")" ]] \
|
|
|| fail 'ambient GPG snapshot ignored unexpected file content'
|
|
printf 'baseline-value\n' > "$ambient_gnupg/unexpected-entry"
|
|
ambient_gpg_files_before="$(snapshot_gpg_state "$ambient_gnupg")"
|
|
before_gnupg="$(snapshot "$host_gnupg")"
|
|
before_gpg_files="$(snapshot_gpg_state "$host_gnupg")"
|
|
before_rpmdb="$(snapshot "$host_rpmdb")"
|
|
before_repo_files="$(snapshot_gpg_state /etc/yum.repos.d)"
|
|
before_rpm_key_files="$(snapshot_gpg_state /etc/pki/rpm-gpg)"
|
|
before_system_flatpak="$(snapshot_file_state /var/lib/flatpak/repo/config)"
|
|
before_system_flathub_key="$(snapshot_file_state /var/lib/flatpak/repo/flathub.trustedkeys.gpg)"
|
|
before_user_flatpak="$(snapshot_file_state "$HOME/.local/share/flatpak/repo/config")"
|
|
before_bashrc="$(snapshot_file_state "$HOME/.bashrc")"
|
|
|
|
# Runtime and agent installs must consume the reviewed provenance table. Keep
|
|
# this scan at the public script boundary because a command hidden elsewhere in
|
|
# the installer can bypass every archive-level test below.
|
|
installer="$repo_dir/setup/scripts/install-packages"
|
|
grep -qFx 'PRIVILEGED_TMPDIR=/var/tmp' "$installer" \
|
|
|| fail 'privileged staging parent is selectable through caller environment'
|
|
for openh264_binding in 'fedora-cisco-openh264.repo' \
|
|
'--repo=fedora-cisco-openh264' '--from-repo=fedora-cisco-openh264'; do
|
|
grep -qF -- "$openh264_binding" "$installer" \
|
|
|| fail "desktop repository scope omits $openh264_binding"
|
|
done
|
|
if grep -qF 'DESKTOP_REPO_ARGS=("${RPMFUSION_REPO_ARGS[@]}" --repo=terra)' "$installer" \
|
|
|| grep -qF 'HYPRLAND_REPO_ARGS=("${DESKTOP_REPO_ARGS[@]}"' "$installer"; then
|
|
fail 'Terra remains in a broad desktop or Hyprland repository scope'
|
|
fi
|
|
multimedia_scope="$(sed -n '/^# --- Codecs and multimedia/,/^# --- Install Development Packages/p' "$installer")"
|
|
[[ "$multimedia_scope" != *'HYPRLAND_REPO_ARGS'* \
|
|
&& "$multimedia_scope" != *'--repo=terra'* \
|
|
&& "$multimedia_scope" != *'--repo=panama-hyprland'* ]] \
|
|
|| fail 'multimedia transactions admit Terra or the Hyprland COPR'
|
|
unsafe_installers=()
|
|
for forbidden in \
|
|
'curl[^|]*\|[[:space:]]*bash' \
|
|
'nvm[[:space:]]+install[[:space:]]+--lts' \
|
|
'npm[[:space:]]+install[[:space:]]+-g[[:space:]]+pnpm' \
|
|
'npm[[:space:]]+install[[:space:]]+-g[[:space:]]+@openai/codex' \
|
|
'releases/latest' \
|
|
'api\.github\.com/.*/releases/latest'; do
|
|
while IFS= read -r finding; do
|
|
[[ -n "$finding" ]] && unsafe_installers+=("$finding")
|
|
done < <(grep -nE "$forbidden" "$installer" || true)
|
|
done
|
|
if (( ${#unsafe_installers[@]} > 0 )); then
|
|
printf 'package provenance contract: moving or piped installer inputs:\n' >&2
|
|
printf ' %s\n' "${unsafe_installers[@]}" >&2
|
|
fail 'replace each finding with a reviewed, verified installation path'
|
|
fi
|
|
|
|
# Every package-solving DNF command declares its dependency source set. An
|
|
# unrelated enabled operator repository may remain configured, but it cannot
|
|
# participate in a Panama transaction merely because DNF discovered it.
|
|
unscoped_dnf="$(python3 - "$repo_dir/install" "$installer" <<'PY'
|
|
import sys
|
|
|
|
for path in sys.argv[1:]:
|
|
lines = open(path, encoding="utf-8").read().splitlines()
|
|
index = 0
|
|
while index < len(lines):
|
|
command = lines[index].strip()
|
|
start = index + 1
|
|
while command.endswith("\\") and index + 1 < len(lines):
|
|
command = command[:-1] + " " + lines[index + 1].strip()
|
|
index += 1
|
|
index += 1
|
|
if "sudo dnf" not in command or " config-manager " in command:
|
|
continue
|
|
if "--repo=" not in command and "REPO_ARGS" not in command:
|
|
print(f"{path}:{start}:{command}")
|
|
PY
|
|
)"
|
|
[[ -z "$unscoped_dnf" ]] || fail "unscoped DNF transaction(s): $unscoped_dnf"
|
|
|
|
# Re-running install-packages must be keyed to every reviewed trust input it
|
|
# consumes. The update-command fixture proves each input changes the digest;
|
|
# this public-boundary guard keeps any of those inputs from being silently
|
|
# removed from the installer state definition.
|
|
for state_input in \
|
|
'setup/packages' \
|
|
'setup/scripts/install-packages' \
|
|
'setup/lib/artifact-provenance' \
|
|
'setup/lib/extras-catalog' \
|
|
'setup/lib/machine-role' \
|
|
'setup/provenance'; do
|
|
grep -Fq "$state_input" "$repo_dir/install" \
|
|
|| fail "packages hash does not name required state input: $state_input"
|
|
done
|
|
|
|
# This must be the only production file sourced by the contract.
|
|
# shellcheck source=../../setup/lib/artifact-provenance
|
|
source "$repo_dir/setup/lib/artifact-provenance"
|
|
|
|
fixture_fingerprint='6016FF18CAE298CE3648EE2325E01F765E1EF9FA'
|
|
tiny_sha256='291bd319ae85488101e908e37fc0fa1b0da1429ba27e10d2b391cb3f60dd44ea'
|
|
base64 --decode "$fixtures/signed-fixture.rpm.base64" > "$test_tmp/signed-fixture.rpm"
|
|
base64 --decode "$fixtures/unsigned-fixture.rpm.base64" > "$test_tmp/unsigned-fixture.rpm"
|
|
base64 --decode "$fixtures/wrong-signer-fixture.rpm.base64" > "$test_tmp/wrong-signer-fixture.rpm"
|
|
|
|
# The helper owns the producer-status check; it cannot depend on a sourcing
|
|
# script having enabled pipefail before the GPG-to-parser pipeline runs.
|
|
gpg() {
|
|
printf 'pub:-:4096:1:0000000000000000:0:0::-:::scESC::::::23::0:\n'
|
|
printf 'fpr:::::::::%s:\n' "$fixture_fingerprint"
|
|
return 42
|
|
}
|
|
set +o pipefail
|
|
expect_failure _primary_key_fingerprints "$fixtures/fixture-key.asc"
|
|
set -o pipefail
|
|
unset -f gpg
|
|
|
|
[[ "$(wc -l < "$fixtures/SHASUMS256.txt")" -eq 4 ]] || fail 'signed checksum fixture is not four lines'
|
|
[[ "$(cmp -l "$fixtures/tiny-artifact" "$fixtures/tiny-artifact-tampered" | wc -l)" -eq 1 ]] \
|
|
|| fail 'tampered artifact does not differ by exactly one byte'
|
|
|
|
expect_success key_fingerprint_matches "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
[[ "$ambient_before" == "$(snapshot "$ambient_gnupg")" ]] \
|
|
|| fail 'fingerprint inspection created ambient GPG state'
|
|
[[ "$ambient_gpg_files_before" == "$(snapshot_gpg_state "$ambient_gnupg")" ]] \
|
|
|| fail 'fingerprint inspection created an ambient GPG keybox or trust database'
|
|
expect_failure key_fingerprint_matches "$fixtures/fixture-key.asc" '0000000000000000000000000000000000000000'
|
|
cat "$fixtures/fixture-key.asc" "$fixtures/wrong-signer-key.asc" > "$test_tmp/combined-key.asc"
|
|
expect_failure key_fingerprint_matches "$test_tmp/combined-key.asc" "$fixture_fingerprint"
|
|
|
|
# A producer can print a valid-looking primary fingerprint and still fail on
|
|
# later malformed input. Public matchers must preserve that failure rather
|
|
# than returning mapfile's successful process-substitution status.
|
|
primary_key_fingerprints_definition="$(declare -f _primary_key_fingerprints)"
|
|
_primary_key_fingerprints() {
|
|
printf '%s\n' "$fixture_fingerprint"
|
|
return 42
|
|
}
|
|
expect_failure key_fingerprint_matches "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
|
eval "$primary_key_fingerprints_definition"
|
|
|
|
cat "$fixtures/fixture-key.asc" > "$test_tmp/malformed-key.asc"
|
|
printf '\n-----BEGIN PGP PUBLIC KEY BLOCK-----\ninvalid\n' >> "$test_tmp/malformed-key.asc"
|
|
malformed_status=0
|
|
malformed_home="$test_tmp/malformed-gnupg"
|
|
mkdir -m 700 "$malformed_home"
|
|
malformed_gpg_output="$(GNUPGHOME="$malformed_home" gpg --batch --with-colons \
|
|
--import-options show-only --import "$test_tmp/malformed-key.asc" 2>/dev/null)" \
|
|
|| malformed_status=$?
|
|
awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }' \
|
|
<<<"$malformed_gpg_output" > "$test_tmp/malformed-key.fingerprints"
|
|
(( malformed_status != 0 )) || fail 'malformed GPG fixture did not exercise a producer failure'
|
|
[[ "$(<"$test_tmp/malformed-key.fingerprints")" == "$fixture_fingerprint" ]] \
|
|
|| fail 'malformed GPG fixture did not emit the valid-looking partial fingerprint'
|
|
expect_failure key_fingerprint_matches "$test_tmp/malformed-key.asc" "$fixture_fingerprint"
|
|
expect_failure verify_detached_signature \
|
|
"$test_tmp/malformed-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
|
|
|
expect_success verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/tiny-artifact"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$fixtures/SHASUMS256.txt.asc" "$fixtures/tiny-artifact-tampered"
|
|
cp "$fixtures/SHASUMS256.txt.asc" "$test_tmp/bad-signature.asc"
|
|
sed -i 's/^=MJqv$/=MJqa/' "$test_tmp/bad-signature.asc"
|
|
expect_failure verify_detached_signature \
|
|
"$fixtures/fixture-key.asc" "$test_tmp/bad-signature.asc" "$fixtures/SHASUMS256.txt"
|
|
expect_failure verify_detached_signature \
|
|
"$test_tmp/combined-key.asc" "$fixtures/wrong-signer-SHASUMS256.txt.asc" "$fixtures/SHASUMS256.txt"
|
|
|
|
mkdir "$test_tmp/bin"
|
|
cat > "$test_tmp/bin/curl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
output=''
|
|
connect_timeout=''
|
|
max_time=''
|
|
max_filesize=''
|
|
while (($#)); do
|
|
case "$1" in
|
|
--output) output="$2"; shift 2 ;;
|
|
--connect-timeout) connect_timeout="$2"; shift 2 ;;
|
|
--max-time) max_time="$2"; shift 2 ;;
|
|
--max-filesize) max_filesize="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
[[ "$connect_timeout" == 10 && "$max_time" == 600 && "$max_filesize" == "${CURL_EXPECTED_MAX_BYTES:?}" ]] || exit 65
|
|
case "${CURL_FIXTURE:?}" in
|
|
good) cp "$CURL_FIXTURE_ROOT/tiny-artifact" "$output" ;;
|
|
oversized) head -c 1025 /dev/zero > "$output" ;;
|
|
interrupted) printf 'partial' > "$output"; exit 42 ;;
|
|
*) exit 64 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$test_tmp/bin/curl"
|
|
PATH="$test_tmp/bin:$PATH"
|
|
export PATH CURL_FIXTURE_ROOT="$fixtures" CURL_EXPECTED_MAX_BYTES=1024
|
|
|
|
destination="$test_tmp/destination"
|
|
export CURL_FIXTURE=good
|
|
expect_success download_sha256 'https://fixture.invalid/good' "$tiny_sha256" 1024 "$destination"
|
|
cmp -s "$fixtures/tiny-artifact" "$destination" || fail 'verified download changed artifact bytes'
|
|
[[ ! -e "$destination.part" ]] || fail 'successful download left a part file'
|
|
|
|
export CURL_FIXTURE=good
|
|
expect_failure download_sha256 'https://fixture.invalid/uppercase-digest' \
|
|
'291BD319AE85488101E908E37FC0FA1B0DA1429BA27E10D2B391CB3F60DD44EA' 1024 \
|
|
"$test_tmp/uppercase-destination"
|
|
|
|
printf 'known-good\n' > "$destination"
|
|
export CURL_FIXTURE=good
|
|
expect_failure download_sha256 'https://fixture.invalid/bad-digest' \
|
|
'0000000000000000000000000000000000000000000000000000000000000000' 1024 "$destination"
|
|
assert_file_bytes "$destination" 'known-good'
|
|
[[ ! -e "$destination.part" ]] || fail 'bad digest left a part file'
|
|
|
|
protected="$test_tmp/protected-known-good"
|
|
printf 'known-good\n' > "$protected"
|
|
ln -s "$protected" "$destination.part"
|
|
expect_failure download_sha256 'https://fixture.invalid/symlink-part' \
|
|
'0000000000000000000000000000000000000000000000000000000000000000' 1024 "$destination"
|
|
assert_file_bytes "$protected" 'known-good'
|
|
[[ -L "$destination.part" ]] || fail 'symlinked destination part was not preserved'
|
|
rm -f -- "$destination.part"
|
|
|
|
ln "$protected" "$destination.part"
|
|
expect_failure download_sha256 'https://fixture.invalid/hard-link-part' \
|
|
'0000000000000000000000000000000000000000000000000000000000000000' 1024 "$destination"
|
|
assert_file_bytes "$protected" 'known-good'
|
|
[[ "$(stat -c %i "$protected")" == "$(stat -c %i "$destination.part")" ]] \
|
|
|| fail 'hard-linked destination part was not preserved'
|
|
rm -f -- "$destination.part"
|
|
|
|
export CURL_FIXTURE=oversized
|
|
expect_failure download_sha256 'https://fixture.invalid/oversized' "$tiny_sha256" 1024 "$destination"
|
|
assert_file_bytes "$destination" 'known-good'
|
|
[[ ! -e "$destination.part" ]] || fail 'oversized download left a part file'
|
|
|
|
export CURL_FIXTURE=interrupted
|
|
expect_failure download_sha256 'https://fixture.invalid/interrupted' "$tiny_sha256" 1024 "$destination"
|
|
assert_file_bytes "$destination" 'known-good'
|
|
[[ ! -e "$destination.part" ]] || fail 'interrupted download left a part file'
|
|
|
|
expect_success rpm_signature_matches \
|
|
"$test_tmp/signed-fixture.rpm" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/unsigned-fixture.rpm" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/signed-fixture.rpm" "$fixtures/fixture-key.asc" '0000000000000000000000000000000000000000'
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/wrong-signer-fixture.rpm" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure rpm_signature_matches \
|
|
"$test_tmp/wrong-signer-fixture.rpm" "$test_tmp/combined-key.asc" "$fixture_fingerprint"
|
|
expect_success load_installer_provenance "$config"
|
|
[[ "${INSTALLER_PROVENANCE[BUN_VERSION]:-}" == '1.4.0' ]] || fail 'valid provenance was not loaded'
|
|
for reviewed_value in \
|
|
'NODE_VERSION 24.20.0' \
|
|
'NODE_X86_64_SHA256 2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2' \
|
|
'NODE_X86_64_BINARY_SHA256 89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7' \
|
|
'NODE_AARCH64_SHA256 5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7' \
|
|
'NODE_AARCH64_BINARY_SHA256 23a5637c2470fde09fcc1acc77c1b92e04e3d7e3e6e80ff7df6f5831958d1477' \
|
|
'BUN_VERSION 1.4.0' \
|
|
'BUN_X86_64_SHA256 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
|
'BUN_X86_64_BINARY_SHA256 33d56b070be6a9e3da0ab013038b43d1645d0534ca811ecdba4472599117eb4b' \
|
|
'BUN_AARCH64_SHA256 4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e' \
|
|
'BUN_AARCH64_BINARY_SHA256 086c4121c8738a8e0f5ed730e8a461bc3973b4444e372ddb77aef9a747fa2ae9' \
|
|
'CODEX_VERSION 0.150.1' \
|
|
'CODEX_X86_64_SHA256 00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17' \
|
|
'CODEX_X86_64_BINARY_SHA256 abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386' \
|
|
'CODEX_AARCH64_SHA256 1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639' \
|
|
'CODEX_AARCH64_BINARY_SHA256 7a49aabe11fd95a1c968d79e16b5f1b17c3219002c5f7129d467f415f8460feb' \
|
|
'RUSTDESK_VERSION 1.4.9' \
|
|
'RUSTDESK_X86_64_SHA256 eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108'; do
|
|
read -r name expected <<<"$reviewed_value"
|
|
[[ "${INSTALLER_PROVENANCE[$name]:-}" == "$expected" ]] \
|
|
|| fail "$name does not match the reviewed release"
|
|
done
|
|
for key_spec in \
|
|
'terra44 TERRA_FINGERPRINT' \
|
|
'claude-code CLAUDE_CODE_FINGERPRINT' \
|
|
'bun BUN_FINGERPRINT' \
|
|
'rpmfusion-free RPMFUSION_FREE_FINGERPRINT' \
|
|
'rpmfusion-nonfree RPMFUSION_NONFREE_FINGERPRINT' \
|
|
'hyprland-copr HYPRLAND_COPR_FINGERPRINT' \
|
|
'flathub FLATHUB_FINGERPRINT' \
|
|
'claude-desktop CLAUDE_DESKTOP_FINGERPRINT'; do
|
|
read -r key_file fingerprint_name <<<"$key_spec"
|
|
expect_success key_fingerprint_matches "$repo_dir/setup/provenance/keys/$key_file.asc" \
|
|
"${INSTALLER_PROVENANCE[$fingerprint_name]}"
|
|
done
|
|
|
|
[[ "$before_gnupg" == "$(snapshot "$host_gnupg")" ]] || fail 'host GPG state changed'
|
|
[[ "$before_gpg_files" == "$(snapshot_gpg_state "$host_gnupg")" ]] || fail 'host GPG state changed'
|
|
[[ "$before_rpmdb" == "$(snapshot "$host_rpmdb")" ]] || fail 'host RPM database changed'
|
|
[[ "$ambient_before" == "$(snapshot "$ambient_gnupg")" ]] || fail 'production helper created ambient GPG state'
|
|
[[ "$ambient_gpg_files_before" == "$(snapshot_gpg_state "$ambient_gnupg")" ]] \
|
|
|| fail 'production helper created or changed an ambient GPG entry'
|
|
|
|
parser_fixture="$test_tmp/installers.conf"
|
|
cp "$config" "$parser_fixture"
|
|
printf 'UNKNOWN_KEY=value\n' >> "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
printf 'BUN_VERSION=1.4.0\n' >> "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
grep -v '^NODE_VERSION=' "$config" > "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
sed -i 's/^BUN_VERSION=/ BUN_VERSION=/' "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
sed -i 's/^BUN_VERSION=.*/BUN_VERSION=$(id -u)/' "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
cp "$config" "$parser_fixture"
|
|
printf 'BUN_ARMV7_URL=https://fixture.invalid/bun\n' >> "$parser_fixture"
|
|
expect_failure load_installer_provenance "$parser_fixture"
|
|
|
|
# Repository setup runs from a fixture copy of the installer with every
|
|
# external command replaced. A contract failure can therefore inspect exact
|
|
# ordering and staged bytes without consulting or changing the host.
|
|
installer_fixture="$test_tmp/installer-fixture"
|
|
mkdir -p "$installer_fixture/setup/lib" "$installer_fixture/setup/provenance/keys" \
|
|
"$installer_fixture/setup/scripts"
|
|
cp "$repo_dir/setup/lib/artifact-provenance" "$repo_dir/setup/lib/extras-catalog" \
|
|
"$repo_dir/setup/lib/machine-role" "$installer_fixture/setup/lib/"
|
|
cp "$config" "$installer_fixture/setup/provenance/installers.conf"
|
|
cp "$repo_dir"/setup/provenance/keys/*.asc "$installer_fixture/setup/provenance/keys/"
|
|
sed '/^# --- The server path/,$d' "$repo_dir/setup/scripts/install-packages" \
|
|
> "$installer_fixture/setup/scripts/install-packages"
|
|
cat >> "$installer_fixture/setup/scripts/install-packages" <<'STUB'
|
|
|
|
exercise_agent_install_boundary() {
|
|
setup_node() { printf 'agent:node\n' >> "$COMMAND_LOG"; }
|
|
install_pnpm() { printf 'agent:pnpm\n' >> "$COMMAND_LOG"; }
|
|
install_bun() { printf 'agent:bun\n' >> "$COMMAND_LOG"; }
|
|
install_claude_code() { printf 'agent:claude:78\n' >> "$COMMAND_LOG"; return 78; }
|
|
install_codex() { printf 'agent:codex\n' >> "$COMMAND_LOG"; }
|
|
install_optional_agent_tools
|
|
}
|
|
STUB
|
|
|
|
artifact_root="$test_tmp/runtime-artifacts"
|
|
mkdir -p "$artifact_root/build"
|
|
for arch_spec in \
|
|
'x86_64 x64 x64' \
|
|
'aarch64 arm64 aarch64'; do
|
|
read -r machine node_arch bun_arch <<<"$arch_spec"
|
|
node_top="node-v24.20.0-linux-$node_arch"
|
|
mkdir -p "$artifact_root/build/$node_top/bin"
|
|
printf '#!/usr/bin/env bash\n# fixture %s\nprintf "v24.20.0\\n"\n' "$machine" \
|
|
> "$artifact_root/build/$node_top/bin/node"
|
|
chmod +x "$artifact_root/build/$node_top/bin/node"
|
|
cp "$artifact_root/build/$node_top/bin/node" "$artifact_root/node-$machine.bin"
|
|
tar -C "$artifact_root/build" -cJf "$artifact_root/node-$machine.tar.xz" "$node_top"
|
|
rm -rf -- "$artifact_root/build/$node_top"
|
|
|
|
bun_top="bun-linux-$bun_arch"
|
|
mkdir -p "$artifact_root/build/$bun_top"
|
|
printf '#!/usr/bin/env bash\n# fixture %s\nprintf "1.4.0\\n"\n' "$machine" \
|
|
> "$artifact_root/build/$bun_top/bun"
|
|
chmod +x "$artifact_root/build/$bun_top/bun"
|
|
cp "$artifact_root/build/$bun_top/bun" "$artifact_root/bun-$machine.bin"
|
|
(cd "$artifact_root/build" && zip -q "$artifact_root/bun-$machine.zip" \
|
|
"$bun_top/" "$bun_top/bun")
|
|
rm -rf -- "$artifact_root/build/$bun_top"
|
|
|
|
mkdir -p "$artifact_root/build/bin" "$artifact_root/build/codex-path" \
|
|
"$artifact_root/build/codex-resources/zsh/bin"
|
|
printf '#!/usr/bin/env bash\n# fixture %s\nprintf "codex-cli 0.150.1\\n"\n' "$machine" \
|
|
> "$artifact_root/build/bin/codex"
|
|
cp "$artifact_root/build/bin/codex" "$artifact_root/codex-$machine.bin"
|
|
printf '#!/usr/bin/env bash\nprintf "code mode host\\n"\n' \
|
|
> "$artifact_root/build/bin/codex-code-mode-host"
|
|
printf '{"target":"%s"}\n' "$machine" > "$artifact_root/build/codex-package.json"
|
|
for package_binary in codex-path/rg codex-resources/bwrap codex-resources/zsh/bin/zsh; do
|
|
printf '#!/usr/bin/env bash\nprintf "package resource\\n"\n' \
|
|
> "$artifact_root/build/$package_binary"
|
|
done
|
|
chmod +x "$artifact_root/build/bin/codex" \
|
|
"$artifact_root/build/bin/codex-code-mode-host" \
|
|
"$artifact_root/build/codex-path/rg" "$artifact_root/build/codex-resources/bwrap" \
|
|
"$artifact_root/build/codex-resources/zsh/bin/zsh"
|
|
tar -C "$artifact_root/build" --no-recursion -czf "$artifact_root/codex-$machine.tar.gz" \
|
|
bin/ bin/codex bin/codex-code-mode-host codex-package.json \
|
|
codex-path/ codex-path/rg codex-resources/ codex-resources/bwrap \
|
|
codex-resources/zsh/ codex-resources/zsh/bin/ codex-resources/zsh/bin/zsh
|
|
rm -rf -- "$artifact_root/build/bin" "$artifact_root/build/codex-path" \
|
|
"$artifact_root/build/codex-resources"
|
|
rm -f -- "$artifact_root/build/codex-package.json"
|
|
done
|
|
python3 - "$artifact_root/bun-symlink.zip" "$artifact_root/bun-special.zip" \
|
|
"$artifact_root/bun-directory.zip" <<'PY'
|
|
import stat
|
|
import sys
|
|
import zipfile
|
|
|
|
for destination, directory_type, entry_type, content in (
|
|
(sys.argv[1], stat.S_IFDIR, stat.S_IFLNK, b"../../outside-bun"),
|
|
(sys.argv[2], stat.S_IFDIR, stat.S_IFSOCK, b'#!/usr/bin/env bash\nprintf "1.4.0\\n"\n'),
|
|
(sys.argv[3], stat.S_IFREG, stat.S_IFREG, b'#!/usr/bin/env bash\nprintf "1.4.0\\n"\n'),
|
|
):
|
|
with zipfile.ZipFile(destination, "w") as archive:
|
|
directory = zipfile.ZipInfo("bun-linux-x64/")
|
|
directory.create_system = 3
|
|
directory.external_attr = (directory_type | 0o755) << 16
|
|
archive.writestr(directory, b"")
|
|
binary = zipfile.ZipInfo("bun-linux-x64/bun")
|
|
binary.create_system = 3
|
|
binary.external_attr = (entry_type | 0o755) << 16
|
|
archive.writestr(binary, content)
|
|
PY
|
|
mkdir -p "$artifact_root/build/wrong-node/bin" "$artifact_root/build/wrong-codex"
|
|
printf '#!/usr/bin/env bash\nprintf "v24.20.0\\n"\n' > "$artifact_root/build/wrong-node/bin/node"
|
|
printf '#!/usr/bin/env bash\nprintf "codex-cli 0.150.1\\n"\n' > "$artifact_root/build/wrong-codex/codex"
|
|
chmod +x "$artifact_root/build/wrong-node/bin/node" "$artifact_root/build/wrong-codex/codex"
|
|
tar -C "$artifact_root/build" -cJf "$artifact_root/node-bad.tar.xz" wrong-node
|
|
tar -C "$artifact_root/build" -czf "$artifact_root/codex-bad.tar.gz" wrong-codex/codex
|
|
mkdir -p "$artifact_root/build/bun-linux-x64"
|
|
printf '#!/usr/bin/env bash\nprintf "1.4.0\\n"\n' > "$artifact_root/build/bun-linux-x64/bun"
|
|
printf 'unexpected\n' > "$artifact_root/build/bun-linux-x64/extra"
|
|
chmod +x "$artifact_root/build/bun-linux-x64/bun"
|
|
(cd "$artifact_root/build" && zip -q "$artifact_root/bun-bad.zip" \
|
|
bun-linux-x64/ bun-linux-x64/bun bun-linux-x64/extra)
|
|
cp "$test_tmp/unsigned-fixture.rpm" "$artifact_root/rustdesk.rpm"
|
|
printf '#!/usr/bin/env bash\nprintf "executed\\n" > "$OUTSIDE_EXECUTED"\nprintf "v24.20.0\\n"\n' \
|
|
> "$artifact_root/outside-node"
|
|
printf '#!/usr/bin/env bash\nprintf "executed\\n" > "$OUTSIDE_EXECUTED"\nprintf "codex-cli 0.150.1\\n"\n' \
|
|
> "$artifact_root/outside-codex"
|
|
chmod +x "$artifact_root/outside-node" "$artifact_root/outside-codex"
|
|
node_escape_top='node-v24.20.0-linux-x64'
|
|
mkdir -p "$artifact_root/build/$node_escape_top/bin"
|
|
ln -s "$artifact_root/outside-node" "$artifact_root/build/$node_escape_top/bin/node"
|
|
tar -C "$artifact_root/build" -cJf "$artifact_root/node-symlink-escape.tar.xz" "$node_escape_top"
|
|
rm -rf -- "$artifact_root/build"
|
|
|
|
make_stub_commands() {
|
|
local case_root="$1"
|
|
mkdir -p "$case_root/bin" "$case_root/home" "$case_root/tmp" \
|
|
"$case_root/etc/profile.d" "$case_root/etc/yum.repos.d" \
|
|
"$case_root/etc/pki/rpm-gpg" "$case_root/flatpak-repo" \
|
|
"$case_root/root-staging"
|
|
|
|
cat > "$case_root/etc/profile.d/nvm.sh" <<'STUB'
|
|
nvm() {
|
|
printf 'nvm:%s\n' "$*" >> "$COMMAND_LOG"
|
|
}
|
|
STUB
|
|
|
|
cat > "$case_root/bin/uname" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
printf '%s\n' "${STUB_ARCH:-x86_64}"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/stat" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "${1:-}" == -c && "${2:-}" == '%u:%a' && "${3:-}" == -- \
|
|
&& ( "${4:-}" == "$STUB_ETC"/* || "${4:-}" == "$STUB_FLATPAK_REPO"/* ) ]]; then
|
|
[[ -f "$4" && ! -L "$4" ]] || exit 1
|
|
printf '0:%s\n' "$(/usr/bin/stat -c %a "$4")"
|
|
exit 0
|
|
fi
|
|
exec /usr/bin/stat "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/mktemp" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == gpg-home && "${1:-}" == -d ]]; then
|
|
directory="$(/usr/bin/mktemp "$@")"
|
|
printf '%s\n' "$directory"
|
|
printf 'signal:gpg-home\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == repo-work && "${1:-}" == -d \
|
|
&& "$*" != *'-u'* ]]; then
|
|
directory="$(/usr/bin/mktemp "$@")"
|
|
printf '%s\n' "$directory"
|
|
printf 'signal:repo-work\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
exec /usr/bin/mktemp "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/mkdir" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
target="${!#}"
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == gpg-home \
|
|
&& "$(basename -- "$target")" == panama-gpg.* ]]; then
|
|
/usr/bin/mkdir "$@"
|
|
printf 'signal:gpg-home\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == repo-work \
|
|
&& "$(basename -- "$target")" == panama-rpmfusion.* ]]; then
|
|
/usr/bin/mkdir "$@"
|
|
printf 'signal:repo-work\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
exec /usr/bin/mkdir "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/chmod" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
/usr/bin/chmod "$@"
|
|
target="${!#}"
|
|
if [[ "${STUB_SWAP_AFTER_REPO_WRITE:-}" == "$(basename -- "$target")" \
|
|
&& "${1:-}" == 0600 ]]; then
|
|
printf 'swapped after repository write\n' > "$target"
|
|
printf '%s\n' "$target" > "$STUB_SWAP_MARKER"
|
|
fi
|
|
STUB
|
|
|
|
cat > "$case_root/bin/tar" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
/usr/bin/tar "$@"
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == extract && "$*" == *'-x'* ]]; then
|
|
printf 'signal:extract\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ "${STUB_NODE_ESCAPE:-}" == hardlink && "$*" == *'-xJf'* ]]; then
|
|
destination=''
|
|
while (($#)); do
|
|
if [[ "$1" == -C ]]; then
|
|
destination="$2"
|
|
break
|
|
fi
|
|
shift
|
|
done
|
|
[[ -n "$destination" ]] || exit 71
|
|
node="$destination/node-v24.20.0-linux-x64/bin/node"
|
|
rm -f -- "$node"
|
|
ln "$ARTIFACT_ROOT/outside-node" "$node"
|
|
fi
|
|
if [[ -n "${STUB_TRAVERSAL_ERROR:-}" && "$*" == *'-x'* ]]; then
|
|
destination=''
|
|
arguments=("$@")
|
|
for ((index = 0; index < ${#arguments[@]}; index++)); do
|
|
if [[ "${arguments[index]}" == -C ]]; then
|
|
destination="${arguments[index + 1]}"
|
|
break
|
|
fi
|
|
done
|
|
[[ -n "$destination" ]] || exit 73
|
|
case "$STUB_TRAVERSAL_ERROR" in
|
|
Node)
|
|
selected="$destination/node-v24.20.0-linux-x64/bin/node"
|
|
outside="$ARTIFACT_ROOT/outside-node"
|
|
mkdir "$destination/node-v24.20.0-linux-x64/.unreadable"
|
|
chmod 000 "$destination/node-v24.20.0-linux-x64/.unreadable"
|
|
;;
|
|
Codex)
|
|
selected="$destination/bin/codex"
|
|
outside="$ARTIFACT_ROOT/outside-codex"
|
|
;;
|
|
esac
|
|
rm -f -- "$selected"
|
|
ln -s -- "$outside" "$selected"
|
|
fi
|
|
STUB
|
|
|
|
cat > "$case_root/bin/find" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ -n "${STUB_TRAVERSAL_ERROR:-}" && "${1:-}" == *'.stage.'* ]]; then
|
|
printf 'find:traversal-error:%s\n' "$STUB_TRAVERSAL_ERROR" >> "$COMMAND_LOG"
|
|
if [[ "$STUB_TRAVERSAL_ERROR" == Node ]]; then
|
|
root="$1"
|
|
shift
|
|
exec /usr/bin/find "$root/.unreadable" "$@"
|
|
fi
|
|
printf 'malformed traversal output'
|
|
exit 74
|
|
fi
|
|
exec /usr/bin/find "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/unzip" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ -n "${STUB_BUN_ATTRIBUTE:-}" && "${1:-}" == -q ]]; then
|
|
destination=''
|
|
while (($#)); do
|
|
if [[ "$1" == -d ]]; then
|
|
destination="$2"
|
|
break
|
|
fi
|
|
shift
|
|
done
|
|
[[ -n "$destination" ]] || exit 72
|
|
mkdir -p "$destination/bun-linux-x64"
|
|
cp "$ARTIFACT_ROOT/bun-x86_64.bin" "$destination/bun-linux-x64/bun"
|
|
chmod +x "$destination/bun-linux-x64/bun"
|
|
exit 0
|
|
fi
|
|
exec /usr/bin/unzip "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/mv" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
destination="${!#}"
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == activation ]]; then
|
|
case "$destination" in
|
|
*/.nvm/versions/node/v24.20.0|*/.bun/versions/1.4.0|*/.local/lib/panama/codex/0.150.1)
|
|
printf 'signal:activation\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
;;
|
|
esac
|
|
elif [[ "${STUB_SIGNAL_PHASE:-}" == link ]]; then
|
|
case "$destination" in
|
|
*/.bun/bin/bun|*/.local/bin/codex)
|
|
printf 'signal:link\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
;;
|
|
esac
|
|
fi
|
|
case "${STUB_LATE_COLLISION:-}:$destination" in
|
|
Node:*/.nvm/versions/node/v24.20.0|\
|
|
Bun:*/.bun/versions/1.4.0|\
|
|
Codex:*/.local/lib/panama/codex/0.150.1)
|
|
if [[ ! -e "$destination" ]]; then
|
|
mkdir -p -- "$destination"
|
|
printf 'preserved collision\n' > "$destination/collision-marker"
|
|
printf 'mv:late-collision:%s\n' "$STUB_LATE_COLLISION" >> "$COMMAND_LOG"
|
|
fi
|
|
;;
|
|
esac
|
|
exec /usr/bin/mv "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/rpm" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$*" == '-E %fedora' ]]; then
|
|
printf 'rpm:release\n' >> "$COMMAND_LOG"
|
|
printf '%s\n' "${STUB_FEDORA_RELEASE:-44}"
|
|
elif [[ "${1:-}" == -q ]]; then
|
|
package="${!#}"
|
|
printf 'rpm:query:%s\n' "$package" >> "$COMMAND_LOG"
|
|
case "$package" in
|
|
terra-release)
|
|
[[ "${STUB_TERRA_INSTALLED:-0}" == 1 \
|
|
|| -s "${STUB_TERRA_RPM_STATE:?}" ]]
|
|
;;
|
|
claude-desktop-extra) [[ "${STUB_CLAUDE_DESKTOP_INSTALLED:-0}" == 1 ]] ;;
|
|
rustdesk)
|
|
[[ -n "${STUB_RUSTDESK_VERSION:-}" ]] || exit 1
|
|
[[ "$*" != *--queryformat* ]] || printf '%s' "$STUB_RUSTDESK_VERSION"
|
|
;;
|
|
*) exit 1 ;;
|
|
esac
|
|
else
|
|
exit 64
|
|
fi
|
|
STUB
|
|
|
|
cat > "$case_root/bin/curl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
output='' max_filesize='' connect_timeout='' max_time='' url=''
|
|
while (($#)); do
|
|
case "$1" in
|
|
--output) output="$2"; shift 2 ;;
|
|
--max-filesize) max_filesize="$2"; shift 2 ;;
|
|
--connect-timeout) connect_timeout="$2"; shift 2 ;;
|
|
--max-time) max_time="$2"; shift 2 ;;
|
|
--fail|--location) shift ;;
|
|
*) url="$1"; shift ;;
|
|
esac
|
|
done
|
|
[[ -n "$output" && "$connect_timeout" == 10 && "$max_time" == 600 ]] || exit 65
|
|
output_name="$(basename "$output")"
|
|
output_name="${output_name#.}"
|
|
output_name="${output_name%.part.*}"
|
|
printf 'curl:%s:max=%s:output=%s\n' "$url" "$max_filesize" "$output_name" >> "$COMMAND_LOG"
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == download ]]; then
|
|
printf 'partial' > "$output"
|
|
printf 'signal:download\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ "${STUB_DOWNLOAD_INTERRUPT:-}" == 1 ]]; then
|
|
printf 'partial' > "$output"
|
|
exit 42
|
|
fi
|
|
case "$url" in
|
|
*rpmfusion-free*) cp "$SIGNED_RPM" "$output" ;;
|
|
*rpmfusion-nonfree*) cp "$SIGNED_RPM" "$output" ;;
|
|
*flathub.flatpakrepo) cp "$FLATHUB_DESCRIPTOR" "$output" ;;
|
|
*node-v24.20.0-linux-x64.tar.xz)
|
|
if [[ "${STUB_NODE_ESCAPE:-}" == symlink ]]; then
|
|
cp "$ARTIFACT_ROOT/node-symlink-escape.tar.xz" "$output"
|
|
else
|
|
cp "$ARTIFACT_ROOT/${STUB_BAD_LAYOUT:+node-bad.tar.xz}" "$output" 2>/dev/null \
|
|
|| cp "$ARTIFACT_ROOT/node-x86_64.tar.xz" "$output"
|
|
fi
|
|
;;
|
|
*node-v24.20.0-linux-arm64.tar.xz) cp "$ARTIFACT_ROOT/node-aarch64.tar.xz" "$output" ;;
|
|
*bun-linux-x64.zip)
|
|
cp "$ARTIFACT_ROOT/${STUB_BUN_ATTRIBUTE:+bun-$STUB_BUN_ATTRIBUTE.zip}" "$output" 2>/dev/null \
|
|
|| cp "$ARTIFACT_ROOT/${STUB_BAD_LAYOUT:+bun-bad.zip}" "$output" 2>/dev/null \
|
|
|| cp "$ARTIFACT_ROOT/bun-x86_64.zip" "$output"
|
|
;;
|
|
*bun-linux-aarch64.zip) cp "$ARTIFACT_ROOT/bun-aarch64.zip" "$output" ;;
|
|
*codex-package-x86_64-unknown-linux-musl.tar.gz)
|
|
cp "$ARTIFACT_ROOT/${STUB_BAD_LAYOUT:+codex-bad.tar.gz}" "$output" 2>/dev/null \
|
|
|| cp "$ARTIFACT_ROOT/codex-x86_64.tar.gz" "$output"
|
|
;;
|
|
*codex-package-aarch64-unknown-linux-musl.tar.gz) cp "$ARTIFACT_ROOT/codex-aarch64.tar.gz" "$output" ;;
|
|
*rustdesk-1.4.9-0.x86_64.rpm) cp "$ARTIFACT_ROOT/rustdesk.rpm" "$output" ;;
|
|
*) exit 66 ;;
|
|
esac
|
|
STUB
|
|
|
|
cat > "$case_root/bin/sha256sum" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
file="${!#}"
|
|
if [[ "${STUB_TOGGLE_KEY_VERIFY:-}" == "$(basename -- "$file")" \
|
|
&& "$file" != "$PRIVILEGED_TMPDIR"/panama-install.*/* ]]; then
|
|
printf 'attacker key bytes\n' > "$file"
|
|
printf '%s\n' "$file" > "$STUB_SWAP_MARKER"
|
|
fi
|
|
if [[ "${STUB_DIGEST_MISMATCH:-}" == 1 ]]; then
|
|
printf '%064d %s\n' 0 "$file"
|
|
exit 0
|
|
fi
|
|
if [[ "${STUB_TRAVERSAL_ERROR:-}" == Node ]] \
|
|
&& cmp -s "$file" "$ARTIFACT_ROOT/outside-node"; then
|
|
printf '%s %s\n' 89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7 "$file"
|
|
exit 0
|
|
elif [[ "${STUB_TRAVERSAL_ERROR:-}" == Codex ]] \
|
|
&& cmp -s "$file" "$ARTIFACT_ROOT/outside-codex"; then
|
|
printf '%s %s\n' abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386 "$file"
|
|
exit 0
|
|
fi
|
|
for spec in \
|
|
'node-x86_64.tar.xz 2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2' \
|
|
'node-aarch64.tar.xz 5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7' \
|
|
'node-bad.tar.xz 2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2' \
|
|
'node-symlink-escape.tar.xz 2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2' \
|
|
'bun-x86_64.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
|
'bun-aarch64.zip 4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e' \
|
|
'bun-bad.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
|
'bun-symlink.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
|
'bun-special.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
|
'bun-directory.zip 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452' \
|
|
'codex-x86_64.tar.gz 00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17' \
|
|
'codex-aarch64.tar.gz 1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639' \
|
|
'codex-bad.tar.gz 00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17' \
|
|
'rustdesk.rpm eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108'; do
|
|
read -r fixture digest <<<"$spec"
|
|
if cmp -s "$file" "$ARTIFACT_ROOT/$fixture"; then
|
|
if [[ "$fixture" == rustdesk.rpm ]]; then
|
|
stat -c '%d:%i' "$file" > "$VERIFIED_RUSTDESK_INODE"
|
|
fi
|
|
printf '%s %s\n' "$digest" "$file"
|
|
exit 0
|
|
fi
|
|
done
|
|
for binary_spec in \
|
|
'node-x86_64.bin 89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7' \
|
|
'node-aarch64.bin 23a5637c2470fde09fcc1acc77c1b92e04e3d7e3e6e80ff7df6f5831958d1477' \
|
|
'bun-x86_64.bin 33d56b070be6a9e3da0ab013038b43d1645d0534ca811ecdba4472599117eb4b' \
|
|
'bun-aarch64.bin 086c4121c8738a8e0f5ed730e8a461bc3973b4444e372ddb77aef9a747fa2ae9' \
|
|
'codex-x86_64.bin abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386' \
|
|
'codex-aarch64.bin 7a49aabe11fd95a1c968d79e16b5f1b17c3219002c5f7129d467f415f8460feb'; do
|
|
read -r fixture digest <<<"$binary_spec"
|
|
if cmp -s "$file" "$ARTIFACT_ROOT/$fixture"; then
|
|
printf '%s %s\n' "$digest" "$file"
|
|
exit 0
|
|
fi
|
|
done
|
|
/usr/bin/sha256sum "$@"
|
|
STUB
|
|
|
|
cat > "$case_root/bin/gpg" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
key="${!#}"
|
|
fingerprint=''
|
|
toggle_saved=''
|
|
if [[ "${STUB_TOGGLE_KEY_VERIFY:-}" == "$(basename -- "$key")" \
|
|
&& "$key" != "$PRIVILEGED_TMPDIR"/panama-install.*/* ]]; then
|
|
toggle_saved="$(mktemp)"
|
|
cp -- "$key" "$toggle_saved"
|
|
cp -- "$REVIEWED_KEYS/$(basename -- "$key")" "$key"
|
|
fi
|
|
for candidate in "$REVIEWED_KEYS"/*.asc; do
|
|
if cmp -s "$key" "$candidate"; then
|
|
case "$(basename "$candidate")" in
|
|
terra44.asc) fingerprint='AE09157A4DE88B497EA1D5D300CDAB43DE226D6F' ;;
|
|
claude-code.asc) fingerprint='31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE' ;;
|
|
rpmfusion-free.asc) fingerprint='E9A491A3DE247814E7E067EAE06F8ECDD651FF2E' ;;
|
|
rpmfusion-nonfree.asc) fingerprint='79BDB88F9BBF73910FD4095B6A2AF96194843C65' ;;
|
|
hyprland-copr.asc) fingerprint='97E23476C89635135407C7D5E9BA41342C4B2995' ;;
|
|
flathub.asc) fingerprint='6E5C05D979C76DAF93C081354184DD4D907A7CAE' ;;
|
|
claude-desktop.asc) fingerprint='825A7D15D78BABE45646D5DF382409F597908867' ;;
|
|
esac
|
|
break
|
|
fi
|
|
done
|
|
[[ -n "$fingerprint" ]] || exit 1
|
|
printf 'gpg:fingerprint:%s\n' "$fingerprint" >> "$COMMAND_LOG"
|
|
printf 'pub:-:4096:1:0000000000000000:0:0::-:::scESC::::::23::0:\n'
|
|
printf 'fpr:::::::::%s:\n' "$fingerprint"
|
|
if [[ -n "$toggle_saved" ]]; then
|
|
cp -- "$toggle_saved" "$key"
|
|
rm -f -- "$toggle_saved"
|
|
fi
|
|
STUB
|
|
|
|
cat > "$case_root/bin/rpmkeys" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
action=''
|
|
package=''
|
|
while (($#)); do
|
|
case "$1" in
|
|
--dbpath) shift 2 ;;
|
|
--import) action=import; package="$2"; shift 2 ;;
|
|
--checksig) action=checksig; shift; [[ "${1:-}" == --verbose ]] && shift; package="$1"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
printf 'rpmkeys:%s:%s\n' "$action" "$(basename "$package")" >> "$COMMAND_LOG"
|
|
if [[ "$action" == import \
|
|
&& ( "${STUB_SIGNAL_PHASE:-}" == rpmdb || "${STUB_SIGNAL_PHASE:-}" == root-review ) ]]; then
|
|
printf 'signal:%s\n' "$STUB_SIGNAL_PHASE" >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ "$action" == checksig ]]; then
|
|
[[ "${STUB_RPM_SIGNATURE_FAIL:-}" != "$(basename "$package")" ]] || exit 1
|
|
printf 'Header OpenPGP signature: OK\n'
|
|
fi
|
|
STUB
|
|
|
|
cat > "$case_root/bin/sudo" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "${1:-}" == mktemp && "${2:-}" == -d ]]; then
|
|
printf 'sudo:root-create\n' >> "$COMMAND_LOG"
|
|
directory="$(/usr/bin/mktemp -d "$3")"
|
|
printf '%s\n' "$directory"
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == root-create ]]; then
|
|
printf 'signal:root-create\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
exit 0
|
|
fi
|
|
if [[ "${1:-}" == mkdir && "${2:-}" == -m && "${3:-}" == 0700 \
|
|
&& "${4:-}" == -- ]]; then
|
|
printf 'sudo:root-create\n' >> "$COMMAND_LOG"
|
|
/usr/bin/mkdir -m 0700 -- "$5"
|
|
if [[ "${STUB_SIGNAL_PHASE:-}" == root-create ]]; then
|
|
printf 'signal:root-create\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
exit 0
|
|
fi
|
|
if [[ "${1:-}" == test && "${2:-}" == -d ]]; then
|
|
[[ -d "${3:-}" ]]
|
|
exit
|
|
fi
|
|
if [[ "${1:-}" == test && "${2:-}" == '!' && "${3:-}" == -L ]]; then
|
|
[[ ! -L "${4:-}" ]]
|
|
exit
|
|
fi
|
|
if [[ "${1:-}" == stat && "${2:-}" == -c && "${3:-}" == '%u:%a' \
|
|
&& "${4:-}" == -- ]]; then
|
|
if [[ -d "$5" && ! -L "$5" ]]; then
|
|
printf '0:%s\n' "$(/usr/bin/stat -c %a "$5")"
|
|
elif [[ -f "$5" && ! -L "$5" ]]; then
|
|
printf '0:%s\n' "$(/usr/bin/stat -c %a "$5")"
|
|
else
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
if [[ "${1:-}" == chmod && "${2:-}" == 0700 ]]; then
|
|
printf 'sudo:root-private\n' >> "$COMMAND_LOG"
|
|
exec /usr/bin/chmod 0700 "$3"
|
|
fi
|
|
if [[ "${1:-}" == chmod && "${2:-}" == 0711 ]]; then
|
|
printf 'sudo:root-reviewable\n' >> "$COMMAND_LOG"
|
|
exec /usr/bin/chmod 0711 "$3"
|
|
fi
|
|
if [[ "${1:-}" == sha256sum && "${2:-}" == -- ]]; then
|
|
printf 'sudo:root-verify:%s\n' "$(basename -- "$3")" >> "$COMMAND_LOG"
|
|
source_file="$3"
|
|
[[ "$source_file" != /etc/* ]] || source_file="$STUB_ETC${source_file#/etc}"
|
|
exec sha256sum -- "$source_file"
|
|
fi
|
|
if [[ "${1:-}" == install ]]; then
|
|
shift
|
|
[[ "${1:-}" == -m ]] || exit 67
|
|
mode="$2"
|
|
source_file="$3"
|
|
destination="$4"
|
|
if [[ "$mode" == 0444 || "$mode" == 444 \
|
|
|| "$mode" == 0600 || "$mode" == 600 ]]; then
|
|
[[ "$destination" == "$PRIVILEGED_TMPDIR"/panama-install.*/"$(basename -- "$destination")" ]] \
|
|
|| exit 67
|
|
printf 'sudo:root-stage:%s\n' "$(basename -- "$destination")" >> "$COMMAND_LOG"
|
|
[[ "$source_file" != /etc/* ]] || source_file="$STUB_ETC${source_file#/etc}"
|
|
if [[ "${STUB_TOGGLE_KEY_VERIFY:-}" == "$(basename -- "$source_file")" \
|
|
&& "$source_file" != "$PRIVILEGED_TMPDIR"/panama-install.*/* ]]; then
|
|
printf 'attacker key bytes\n' > "$source_file"
|
|
printf '%s\n' "$source_file" > "$STUB_SWAP_MARKER"
|
|
fi
|
|
/usr/bin/install -m "$mode" "$source_file" "$destination"
|
|
if [[ "${STUB_SWAP_AFTER_ROOT_STAGE:-}" == "$(basename -- "$destination")" ]]; then
|
|
printf 'swapped after privileged copy\n' > "$source_file"
|
|
printf '%s\n' "$source_file" > "$STUB_SWAP_MARKER"
|
|
fi
|
|
exit 0
|
|
fi
|
|
[[ "$mode" == 0644 || "$mode" == 644 ]] || exit 67
|
|
[[ "$source_file" == "$PRIVILEGED_TMPDIR"/panama-install.*/* ]] || exit 67
|
|
printf 'sudo:install:%s:%s\n' "$(basename "$source_file")" "$destination" >> "$COMMAND_LOG"
|
|
mapped="$STUB_ETC${destination#/etc}"
|
|
mkdir -p "$(dirname "$mapped")"
|
|
/usr/bin/install -m 0644 "$source_file" "$mapped"
|
|
count=0
|
|
[[ ! -f "$STUB_INSTALL_COUNTER" ]] || read -r count < "$STUB_INSTALL_COUNTER"
|
|
count=$((count + 1))
|
|
printf '%s\n' "$count" > "$STUB_INSTALL_COUNTER"
|
|
if [[ "${STUB_SIGNAL_PAIR_AFTER_FIRST:-0}" == 1 && "$count" == 1 ]]; then
|
|
printf 'signal:repository-pair\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ "${STUB_SIGNAL_PAIR_TWICE:-0}" == 1 && "$count" == 2 ]]; then
|
|
printf 'signal:repository-pair-second\n' >> "$COMMAND_LOG"
|
|
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
|
kill -TERM -- "-$pgid"
|
|
sleep 2
|
|
fi
|
|
if [[ -n "${STUB_INSTALL_FAIL_AT:-}" && "$count" == "$STUB_INSTALL_FAIL_AT" ]]; then
|
|
exit 67
|
|
fi
|
|
exit 0
|
|
fi
|
|
if [[ "${1:-}" == rm && "${2:-}" == -rf && "${3:-}" == -- ]]; then
|
|
[[ "$4" == "$PRIVILEGED_TMPDIR"/panama-install.* ]] || exit 67
|
|
printf 'sudo:root-cleanup\n' >> "$COMMAND_LOG"
|
|
[[ "${STUB_ROOT_CLEANUP_FAIL:-0}" != 1 ]] || exit 79
|
|
exec /usr/bin/rm -rf -- "$4"
|
|
fi
|
|
if [[ "${1:-}" == rm && "${2:-}" == -f && "${3:-}" == -- ]]; then
|
|
destination="$4"
|
|
printf 'sudo:rm:%s\n' "$destination" >> "$COMMAND_LOG"
|
|
[[ "${STUB_ROLLBACK_FAIL:-0}" != 1 ]] || exit 79
|
|
rm -f -- "$STUB_ETC${destination#/etc}"
|
|
exit 0
|
|
fi
|
|
if [[ "${1:-}" == rpm && "${2:-}" == --import ]]; then
|
|
key="${3:-}"
|
|
[[ "$key" == "$PRIVILEGED_TMPDIR"/panama-install.*/*.asc && -f "$key" ]] \
|
|
|| exit 76
|
|
printf 'sudo:rpm-import:%s\n' "$(basename -- "$key")" >> "$COMMAND_LOG"
|
|
printf '%s\n' "$(basename -- "$key")" >> "$STUB_SYSTEM_KEYRING"
|
|
exit 0
|
|
fi
|
|
original="$*"
|
|
logged=()
|
|
for argument in "$@"; do
|
|
if [[ "$argument" == --gpg-import=*/flathub-key.asc ]]; then
|
|
logged+=(--gpg-import=FLATHUB_KEY)
|
|
continue
|
|
fi
|
|
if [[ "$argument" == --setopt=panama-bound-*.gpgkey=file://*/panama-bound-*.asc ]]; then
|
|
key="${argument#*=file://}"
|
|
[[ "$key" == "$PRIVILEGED_TMPDIR"/panama-install.*/panama-bound-*.asc \
|
|
&& -f "$key" ]] || exit 76
|
|
logged+=("${argument%%=file://*}=file://BOUND_KEY")
|
|
continue
|
|
fi
|
|
if [[ "$argument" == --setopt=panama-claude-desktop.gpgkey=file://*/claude-desktop.asc ]]; then
|
|
key="${argument#*=file://}"
|
|
[[ "$key" == "$PRIVILEGED_TMPDIR"/panama-install.*/claude-desktop.asc \
|
|
&& -f "$key" ]] || exit 76
|
|
logged+=(--setopt=panama-claude-desktop.gpgkey=file://CLAUDE_DESKTOP_KEY)
|
|
continue
|
|
fi
|
|
case "$(basename "$argument")" in
|
|
rpmfusion-free-release.rpm) logged+=(RPMFUSION_FREE) ;;
|
|
rpmfusion-nonfree-release.rpm) logged+=(RPMFUSION_NONFREE) ;;
|
|
rustdesk.rpm) logged+=(RUSTDESK_LOCAL) ;;
|
|
flathub-key.asc) logged+=(FLATHUB_KEY) ;;
|
|
*) logged+=("$argument") ;;
|
|
esac
|
|
done
|
|
printf 'sudo:%s\n' "${logged[*]}" >> "$COMMAND_LOG"
|
|
if [[ "$original" == *'/rustdesk.rpm'* ]]; then
|
|
[[ "$original" == *'--setopt=localpkg_gpgcheck=0'* \
|
|
&& "$original" != *'--setopt=localpkg_gpgcheck=1'* ]] || exit 70
|
|
rustdesk_path=''
|
|
for argument in "$@"; do
|
|
[[ "$(basename -- "$argument")" != rustdesk.rpm ]] || rustdesk_path="$argument"
|
|
done
|
|
[[ "$rustdesk_path" == "$PRIVILEGED_TMPDIR"/panama-install.*/rustdesk.rpm ]] \
|
|
&& cmp -s "$rustdesk_path" "$UNSIGNED_RPM" || exit 72
|
|
/usr/bin/rpm -qp --queryformat '%{NAME}\n' "$rustdesk_path" >/dev/null || exit 73
|
|
signature_status="$(/usr/bin/rpmkeys --checksig --verbose "$rustdesk_path")" || exit 74
|
|
[[ "$signature_status" == *'Header SHA256 digest: OK'* \
|
|
&& "$signature_status" == *'Payload SHA256 digest: OK'* \
|
|
&& "${signature_status,,}" != *signature* ]] || exit 75
|
|
fi
|
|
if [[ "$original" == *'/rpmfusion-free-release.rpm'* \
|
|
|| "$original" == *'/rpmfusion-nonfree-release.rpm'* ]]; then
|
|
if [[ "${STUB_REQUIRE_RPMFUSION_SYSTEM_KEYS:-0}" == 1 ]]; then
|
|
grep -qFx rpmfusion-free.asc "$STUB_SYSTEM_KEYRING" || exit 77
|
|
grep -qFx rpmfusion-nonfree.asc "$STUB_SYSTEM_KEYRING" || exit 77
|
|
fi
|
|
for argument in "$@"; do
|
|
case "$(basename -- "$argument")" in
|
|
rpmfusion-free-release.rpm|rpmfusion-nonfree-release.rpm)
|
|
[[ "$argument" == "$PRIVILEGED_TMPDIR"/panama-install.*/* \
|
|
&& -f "$argument" ]] || exit 76
|
|
cmp -s -- "$argument" "$SIGNED_RPM" || exit 76
|
|
;;
|
|
esac
|
|
done
|
|
fi
|
|
if [[ "$original" == *' pnpm' || "$original" == *' claude-code' \
|
|
|| "$original" == *' claude-desktop-extra' ]]; then
|
|
case "$original" in
|
|
'dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates pnpm'|\
|
|
dnf\ install\ -y\ --repofrompath\ panama-bound-claude-code,https://downloads.claude.ai/claude-code/rpm/stable\ --repo=panama-bound-claude-code\ --repo=fedora\ --repo=updates\ --from-repo=panama-bound-claude-code\ --setopt=panama-bound-claude-code.gpgcheck=1\ --setopt=panama-bound-claude-code.repo_gpgcheck=1\ --setopt=panama-bound-claude-code.gpgkey=file://*/panama-bound-claude-code.asc\ claude-code|\
|
|
dnf\ install\ -y\ --repofrompath\ panama-claude-desktop,https://patrickjaja.github.io/claude-desktop-extra/rpm/\ --repo=panama-claude-desktop\ --repo=fedora\ --repo=updates\ --from-repo=panama-claude-desktop\ --setopt=panama-claude-desktop.gpgcheck=1\ --setopt=panama-claude-desktop.repo_gpgcheck=1\ --setopt=panama-claude-desktop.gpgkey=file://*/claude-desktop.asc\ claude-desktop-extra) ;;
|
|
*) exit 71 ;;
|
|
esac
|
|
fi
|
|
if [[ -n "${STUB_DNF_FAIL_MATCH:-}" && "$original" == *"$STUB_DNF_FAIL_MATCH"* ]]; then
|
|
if [[ "${STUB_TERRA_MUTATE_THEN_FAIL:-0}" == 1 ]]; then
|
|
printf '[terra]\nenabled=1\ngpgcheck=0\nbaseurl=https://evil.invalid/\n' \
|
|
> "$STUB_ETC/yum.repos.d/terra.repo"
|
|
fi
|
|
exit 68
|
|
fi
|
|
if [[ "${STUB_TERRA_MUTATE_ON_SUCCESS:-0}" == 1 \
|
|
&& "$original" == *' terra-release' ]]; then
|
|
printf '[terra]\nenabled=1\ngpgcheck=0\nbaseurl=https://evil.invalid/\n' \
|
|
> "$STUB_ETC/yum.repos.d/terra.repo"
|
|
printf 'terra-release:generated-repo\n' >> "$COMMAND_LOG"
|
|
fi
|
|
if [[ "$original" == *' terra-release' ]]; then
|
|
printf 'installed\n' > "$STUB_TERRA_RPM_STATE"
|
|
fi
|
|
if [[ "${1:-}" == flatpak && "${2:-}" == remote-add ]]; then
|
|
if [[ ! -f "$STUB_FLATPAK_REPO/config" ]] \
|
|
|| ! grep -q '^\[remote "flathub"\]$' "$STUB_FLATPAK_REPO/config"; then
|
|
key=''
|
|
url="${!#}"
|
|
for argument in "$@"; do
|
|
[[ "$argument" != --gpg-import=* ]] || key="${argument#--gpg-import=}"
|
|
done
|
|
[[ -n "$key" ]] || exit 69
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=%s\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
"$url" > "$STUB_FLATPAK_REPO/config"
|
|
cp "$key" "$STUB_FLATPAK_REPO/flathub.trustedkeys.gpg"
|
|
printf 'mutated\n' > "$STUB_FLATPAK_STATE"
|
|
fi
|
|
fi
|
|
STUB
|
|
|
|
cat > "$case_root/bin/dnf" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
query=''
|
|
if [[ "$*" == '--quiet --no-plugins --dump-repo-config=terra*' ]]; then
|
|
query=filtered
|
|
printf 'dnf:dump-terra\n' >> "$COMMAND_LOG"
|
|
elif [[ "$*" == '--quiet --no-plugins --dump-repo-config=*' ]]; then
|
|
query=full
|
|
printf 'dnf:dump-all:locale=%s\n' "${LC_ALL:-unset}" >> "$COMMAND_LOG"
|
|
fi
|
|
if [[ -n "$query" ]]; then
|
|
mode="${STUB_TERRA_EFFECTIVE_MODE:-auto}"
|
|
if [[ "$mode" == auto ]]; then
|
|
case "${STUB_TERRA_REPO_MODE:-absent}" in
|
|
trusted|wrong-key) mode=trusted ;;
|
|
nogpg) mode=legacy ;;
|
|
wrong-url) mode=override-url ;;
|
|
esac
|
|
if [[ "$mode" == auto && -f "$STUB_ETC/yum.repos.d/terra.repo" ]] \
|
|
&& grep -q '^baseurl=https://repos.fyralabs.com/terra44$' "$STUB_ETC/yum.repos.d/terra.repo"; then
|
|
mode=trusted
|
|
elif [[ "$mode" == auto ]]; then
|
|
mode=absent
|
|
fi
|
|
fi
|
|
if [[ "$query" == full ]]; then
|
|
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'
|
|
fi
|
|
case "$mode" in
|
|
absent) exit 0 ;;
|
|
trusted)
|
|
printf '======== "terra" repository configuration: ========\n'
|
|
printf 'baseurl = https://repos.fyralabs.com/terra44\n'
|
|
printf 'enabled = 1\n'
|
|
printf 'gpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
|
;;
|
|
legacy)
|
|
printf '======== "terra" repository configuration: ========\n'
|
|
printf 'baseurl = \nenabled = 1\ngpgcheck = 0\n'
|
|
printf 'gpgkey = https://repos.fyralabs.com/terra44.key\n'
|
|
printf 'metalink = https://tetsudou.fyralabs.com/terra44\nmirrorlist = \n'
|
|
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
|
;;
|
|
override-url)
|
|
printf '======== "terra" repository configuration: ========\n'
|
|
printf 'baseurl = https://evil.invalid/terra44\nenabled = 1\ngpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
|
;;
|
|
override-gpg)
|
|
printf '======== "terra" repository configuration: ========\n'
|
|
printf 'baseurl = https://repos.fyralabs.com/terra44\nenabled = 1\ngpgcheck = 0\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
|
;;
|
|
duplicate)
|
|
for id in terra terra; do
|
|
printf '======== "%s" repository configuration: ========\n' "$id"
|
|
printf 'baseurl = https://repos.fyralabs.com/terra44\nenabled = 1\ngpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
|
done
|
|
;;
|
|
alternate)
|
|
for id in terra terra-legacy; do
|
|
printf '======== "%s" repository configuration: ========\n' "$id"
|
|
printf 'baseurl = https://repos.fyralabs.com/terra44\nenabled = 1\ngpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
|
done
|
|
;;
|
|
locale-unsafe)
|
|
if [[ "${LC_ALL:-}" == C ]]; then
|
|
printf '======== "terra" repository configuration: ========\n'
|
|
else
|
|
printf '======== "terra" Repository-Konfiguration: ========\n'
|
|
fi
|
|
printf 'baseurl = \nenabled = 1\ngpgcheck = 0\n'
|
|
printf 'gpgkey = https://repos.fyralabs.com/terra44.key\n'
|
|
printf 'metalink = https://tetsudou.fyralabs.com/terra44\nmirrorlist = \n'
|
|
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
|
;;
|
|
localized-output)
|
|
printf '======== "terra" Repository-Konfiguration: ========\n'
|
|
printf 'baseurl = \nenabled = 1\ngpgcheck = 0\n'
|
|
;;
|
|
uppercase|mixed-case)
|
|
[[ "$query" == full ]] || exit 0
|
|
[[ "$mode" == uppercase ]] && id=TERRA || id=TeRrA
|
|
printf '======== "%s" repository configuration: ========\n' "$id"
|
|
printf 'baseurl = https://repos.fyralabs.com/terra44\nenabled = 1\ngpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
|
;;
|
|
mixed-alternate)
|
|
ids=(terra)
|
|
[[ "$query" == filtered ]] || ids+=(TeRrA-legacy)
|
|
for id in "${ids[@]}"; do
|
|
printf '======== "%s" repository configuration: ========\n' "$id"
|
|
printf 'baseurl = https://repos.fyralabs.com/terra44\nenabled = 1\ngpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n'
|
|
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\n'
|
|
done
|
|
;;
|
|
esac
|
|
exit 0
|
|
fi
|
|
exit 69
|
|
STUB
|
|
|
|
cat > "$case_root/bin/flatpak" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
exit 69
|
|
STUB
|
|
chmod +x "$case_root/bin"/*
|
|
}
|
|
|
|
reset_installer_fixture() {
|
|
cp "$config" "$installer_fixture/setup/provenance/installers.conf"
|
|
cp "$repo_dir"/setup/provenance/keys/*.asc "$installer_fixture/setup/provenance/keys/"
|
|
}
|
|
|
|
write_flathub_descriptor() {
|
|
local destination="$1" key="$2" verify_line="${3:-}" url="${4:-https://dl.flathub.org/repo/}"
|
|
local encoded
|
|
encoded="$(base64 -w 0 "$key")"
|
|
printf '[Flatpak Repo]\nTitle=Flathub\nUrl=%s\nGPGKey=%s\n%s\n' \
|
|
"$url" "$encoded" "$verify_line" > "$destination"
|
|
}
|
|
|
|
run_installer_function() {
|
|
local name="$1" function_name="$2" case_root
|
|
shift 2
|
|
local -a function_args=("$@")
|
|
case_root="$test_tmp/cases/$name"
|
|
if [[ "${STUB_REUSE_CASE:-}" != 1 ]]; then
|
|
rm -rf -- "$case_root"
|
|
fi
|
|
make_stub_commands "$case_root"
|
|
if [[ "${STUB_OPENH264_REPO:-}" == 1 ]]; then
|
|
printf '[fedora-cisco-openh264]\nenabled=1\n' \
|
|
> "$case_root/etc/yum.repos.d/fedora-cisco-openh264.repo"
|
|
fi
|
|
: > "$case_root/commands.log"
|
|
printf '0\n' > "$case_root/install-counter"
|
|
: > "$case_root/verified-rustdesk-inode"
|
|
: > "$case_root/system-keyring"
|
|
[[ -e "$case_root/terra-rpm-state" ]] || : > "$case_root/terra-rpm-state"
|
|
printf 'preserved\n' > "$case_root/flatpak-state"
|
|
: > "$case_root/softly-failed"
|
|
case "${STUB_SEED_OLD:-}" in
|
|
Node)
|
|
mkdir -p "$case_root/home/.nvm/versions/node/v23.0.0/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "v23.0.0\\n"\n' \
|
|
> "$case_root/home/.nvm/versions/node/v23.0.0/bin/node"
|
|
chmod +x "$case_root/home/.nvm/versions/node/v23.0.0/bin/node"
|
|
;;
|
|
Bun)
|
|
mkdir -p "$case_root/home/.bun/versions/1.3.0/bin" "$case_root/home/.bun/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "1.3.0\\n"\n' \
|
|
> "$case_root/home/.bun/versions/1.3.0/bin/bun"
|
|
chmod +x "$case_root/home/.bun/versions/1.3.0/bin/bun"
|
|
ln -s "$case_root/home/.bun/versions/1.3.0/bin/bun" "$case_root/home/.bun/bin/bun"
|
|
;;
|
|
Codex)
|
|
mkdir -p "$case_root/home/.local/lib/panama/codex/0.149.0" "$case_root/home/.local/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "codex-cli 0.149.0\\n"\n' \
|
|
> "$case_root/home/.local/lib/panama/codex/0.149.0/codex"
|
|
chmod +x "$case_root/home/.local/lib/panama/codex/0.149.0/codex"
|
|
ln -s "$case_root/home/.local/lib/panama/codex/0.149.0/codex" \
|
|
"$case_root/home/.local/bin/codex"
|
|
;;
|
|
esac
|
|
case "${STUB_SEED_LEGACY:-}" in
|
|
Node)
|
|
mkdir -p "$case_root/home/.nvm/versions/node/v24.20.0/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "v24.20.0\\n"\n' \
|
|
> "$case_root/home/.nvm/versions/node/v24.20.0/bin/node"
|
|
chmod +x "$case_root/home/.nvm/versions/node/v24.20.0/bin/node"
|
|
;;
|
|
Bun)
|
|
mkdir -p "$case_root/home/.bun/versions/1.4.0/bin" "$case_root/home/.bun/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "1.4.0\\n"\n' \
|
|
> "$case_root/home/.bun/versions/1.4.0/bin/bun"
|
|
chmod +x "$case_root/home/.bun/versions/1.4.0/bin/bun"
|
|
ln -s "$case_root/home/.bun/versions/1.4.0/bin/bun" "$case_root/home/.bun/bin/bun"
|
|
;;
|
|
Codex)
|
|
mkdir -p "$case_root/home/.local/lib/panama/codex/0.150.1" "$case_root/home/.local/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "codex-cli 0.150.1\\n"\n' \
|
|
> "$case_root/home/.local/lib/panama/codex/0.150.1/codex"
|
|
chmod +x "$case_root/home/.local/lib/panama/codex/0.150.1/codex"
|
|
ln -s "$case_root/home/.local/lib/panama/codex/0.150.1/codex" \
|
|
"$case_root/home/.local/bin/codex"
|
|
;;
|
|
esac
|
|
case "${STUB_SEED_COLLISION:-}" in
|
|
Node)
|
|
mkdir -p "$case_root/home/.nvm/versions/node/v24.20.0/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "v0.0.0\\n"\n' \
|
|
> "$case_root/home/.nvm/versions/node/v24.20.0/bin/node"
|
|
chmod +x "$case_root/home/.nvm/versions/node/v24.20.0/bin/node"
|
|
;;
|
|
Bun)
|
|
mkdir -p "$case_root/home/.bun/versions/1.4.0/bin"
|
|
printf '#!/usr/bin/env bash\nprintf "0.0.0\\n"\n' \
|
|
> "$case_root/home/.bun/versions/1.4.0/bin/bun"
|
|
chmod +x "$case_root/home/.bun/versions/1.4.0/bin/bun"
|
|
;;
|
|
Codex)
|
|
mkdir -p "$case_root/home/.local/lib/panama/codex/0.150.1"
|
|
printf '#!/usr/bin/env bash\nprintf "codex-cli 10.150.10\\n"\n' \
|
|
> "$case_root/home/.local/lib/panama/codex/0.150.1/codex"
|
|
chmod +x "$case_root/home/.local/lib/panama/codex/0.150.1/codex"
|
|
;;
|
|
esac
|
|
write_flathub_descriptor "$case_root/flathub.flatpakrepo" \
|
|
"${STUB_FLATHUB_KEY_FILE:-$installer_fixture/setup/provenance/keys/flathub.asc}" \
|
|
"${STUB_FLATHUB_VERIFY_LINE:-}" "${STUB_FLATHUB_URL:-https://dl.flathub.org/repo/}"
|
|
if [[ "${STUB_EXISTING_REPOSITORY:-}" == hyprland \
|
|
|| "${STUB_PAIR_PRIOR:-}" == present && "${STUB_PAIR_NAME:-}" == hyprland ]]; then
|
|
printf 'known key\n' > "$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland"
|
|
printf 'known repo\n' > "$case_root/etc/yum.repos.d/panama-hyprland.repo"
|
|
fi
|
|
if [[ "${STUB_PAIR_PRIOR:-}" == present && "${STUB_PAIR_NAME:-}" == claude-code ]]; then
|
|
printf 'known key\n' > "$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama"
|
|
printf 'known repo\n' > "$case_root/etc/yum.repos.d/claude-code.repo"
|
|
fi
|
|
case "${STUB_TERRA_REPO_MODE:-absent}" in
|
|
trusted)
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
|
printf '[terra]\nbaseurl=https://repos.fyralabs.com/terra44\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n' \
|
|
> "$case_root/etc/yum.repos.d/terra.repo"
|
|
;;
|
|
trusted-key-symlink)
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$case_root/home/terra44.asc"
|
|
ln -s "$case_root/home/terra44.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
|
printf '[terra]\nbaseurl=https://repos.fyralabs.com/terra44\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n' \
|
|
> "$case_root/etc/yum.repos.d/terra.repo"
|
|
;;
|
|
trusted-repo-symlink)
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
|
printf '[terra]\nbaseurl=https://repos.fyralabs.com/terra44\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n' \
|
|
> "$case_root/home/terra.repo"
|
|
ln -s "$case_root/home/terra.repo" \
|
|
"$case_root/etc/yum.repos.d/terra.repo"
|
|
;;
|
|
nogpg)
|
|
printf '[terra]\nbaseurl=https://repos.fyralabs.com/terra44\nenabled=1\ngpgcheck=0\nrepo_gpgcheck=0\ngpgkey=https://repos.fyralabs.com/terra44.key\n' \
|
|
> "$case_root/etc/yum.repos.d/terra.repo"
|
|
;;
|
|
wrong-url)
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
|
printf '[terra]\nbaseurl=https://evil.invalid/terra44\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n' \
|
|
> "$case_root/etc/yum.repos.d/terra.repo"
|
|
;;
|
|
wrong-key)
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama"
|
|
printf '[terra]\nbaseurl=https://repos.fyralabs.com/terra44\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama\n' \
|
|
> "$case_root/etc/yum.repos.d/terra.repo"
|
|
;;
|
|
esac
|
|
case "${STUB_FLATPAK_REMOTE_MODE:-absent}" in
|
|
trusted)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
trusted-config-symlink)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
> "$case_root/home/flathub-config"
|
|
ln -s "$case_root/home/flathub-config" "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
trusted-key-symlink)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/home/flathub.trustedkeys.gpg"
|
|
ln -s "$case_root/home/flathub.trustedkeys.gpg" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
wrong-url)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://evil.invalid/repo/\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
wrong-key)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
no-gpg)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=false\ngpg-verify-summary=false\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
alternate-key)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=true\ngpg-verify-summary=true\ngpgkeypath=/unreviewed/keyring.gpg\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
empty-alternate-key|duplicate-alternate-key|malformed-alternate-key)
|
|
printf '[core]\nrepo_version=1\n\n[remote "flathub"]\nurl=https://dl.flathub.org/repo/\ngpg-verify=true\ngpg-verify-summary=true\n' \
|
|
> "$case_root/flatpak-repo/config"
|
|
case "$STUB_FLATPAK_REMOTE_MODE" in
|
|
empty-alternate-key) printf 'gpgkeypath=\n' ;;
|
|
duplicate-alternate-key) printf 'gpgkeypath=\ngpgkeypath=/unreviewed/keyring.gpg\n' ;;
|
|
malformed-alternate-key) printf 'gpgkeypath /unreviewed/keyring.gpg\n' ;;
|
|
esac >> "$case_root/flatpak-repo/config"
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/flatpak-repo/flathub.trustedkeys.gpg"
|
|
;;
|
|
esac
|
|
case "${STUB_CLAUDE_DESKTOP_REPO_MODE:-absent}" in
|
|
trusted)
|
|
cp "$installer_fixture/setup/provenance/keys/claude-desktop.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-desktop-panama"
|
|
printf '[claude-desktop]\nbaseurl=https://patrickjaja.github.io/claude-desktop-extra/rpm/\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-claude-desktop-panama\n' \
|
|
> "$case_root/etc/yum.repos.d/claude-desktop.repo"
|
|
;;
|
|
home-key|symlink-repo|metalink|mirrorlist)
|
|
key_target="$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-desktop-panama"
|
|
key_url='file:///etc/pki/rpm-gpg/RPM-GPG-KEY-claude-desktop-panama'
|
|
if [[ "$STUB_CLAUDE_DESKTOP_REPO_MODE" == home-key ]]; then
|
|
key_target="$case_root/home/claude-desktop.asc"
|
|
key_url="file://$key_target"
|
|
fi
|
|
cp "$installer_fixture/setup/provenance/keys/claude-desktop.asc" "$key_target"
|
|
repo_target="$case_root/etc/yum.repos.d/claude-desktop.repo"
|
|
if [[ "$STUB_CLAUDE_DESKTOP_REPO_MODE" == symlink-repo ]]; then
|
|
repo_target="$case_root/home/claude-desktop.repo"
|
|
fi
|
|
printf '[claude-desktop]\nbaseurl=https://patrickjaja.github.io/claude-desktop-extra/rpm/\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file://%s\n' \
|
|
"${key_url#file://}" > "$repo_target"
|
|
case "$STUB_CLAUDE_DESKTOP_REPO_MODE" in
|
|
metalink) printf 'metalink=https://evil.invalid/metadata\n' >> "$repo_target" ;;
|
|
mirrorlist) printf 'mirrorlist=https://evil.invalid/mirrors\n' >> "$repo_target" ;;
|
|
symlink-repo)
|
|
ln -s "$repo_target" "$case_root/etc/yum.repos.d/claude-desktop.repo"
|
|
;;
|
|
esac
|
|
;;
|
|
untrusted)
|
|
cp "$installer_fixture/setup/provenance/keys/claude-desktop.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/claude-desktop-local.asc"
|
|
printf '[claude-desktop]\nbaseurl=https://evil.invalid/rpm/\nenabled=1\ngpgcheck=0\nrepo_gpgcheck=0\ngpgkey=file://%s\n' \
|
|
"$case_root/etc/pki/rpm-gpg/claude-desktop-local.asc" \
|
|
> "$case_root/etc/yum.repos.d/claude-desktop.repo"
|
|
;;
|
|
esac
|
|
|
|
(
|
|
COMMAND_LOG="$case_root/commands.log" \
|
|
SOFT_LOG="$case_root/softly-failed" \
|
|
ARTIFACT_ROOT="$artifact_root" \
|
|
OUTSIDE_EXECUTED="$case_root/outside-executed" \
|
|
FIXTURE_ROOT="$installer_fixture" \
|
|
REVIEWED_KEYS="$repo_dir/setup/provenance/keys" \
|
|
SIGNED_RPM="$test_tmp/signed-fixture.rpm" \
|
|
FLATHUB_DESCRIPTOR="$case_root/flathub.flatpakrepo" \
|
|
STUB_ETC="$case_root/etc" \
|
|
STUB_FLATPAK_STATE="$case_root/flatpak-state" \
|
|
STUB_FLATPAK_REPO="$case_root/flatpak-repo" \
|
|
STUB_INSTALL_COUNTER="$case_root/install-counter" \
|
|
STUB_SYSTEM_KEYRING="$case_root/system-keyring" \
|
|
STUB_TERRA_RPM_STATE="$case_root/terra-rpm-state" \
|
|
STUB_SWAP_MARKER="$case_root/swap-marker" \
|
|
VERIFIED_RUSTDESK_INODE="$case_root/verified-rustdesk-inode" \
|
|
UNSIGNED_RPM="$test_tmp/unsigned-fixture.rpm" \
|
|
LC_ALL="${STUB_CALLER_LOCALE:-C}" \
|
|
HOME="$case_root/home" \
|
|
NVM_DIR="$case_root/home/.nvm" \
|
|
TMPDIR="$case_root/tmp" \
|
|
STUB_PRIVILEGED_TMPDIR="$case_root/root-staging" \
|
|
PANAMA_PATH="$installer_fixture" \
|
|
PATH="$case_root/bin:/usr/bin:/bin" \
|
|
setsid bash -c 'source "$PANAMA_PATH/setup/scripts/install-packages"; PANAMA_SYSTEM_ETC="$STUB_ETC"; PANAMA_SYSTEM_FLATPAK_REPO="$STUB_FLATPAK_REPO"; export PRIVILEGED_TMPDIR="$STUB_PRIVILEGED_TMPDIR"; function_name="$1"; shift; declare -F "$function_name" >/dev/null; status=0; "$function_name" "$@" || status=$?; (( ${#softly_failed[@]} == 0 )) || printf "%s\n" "${softly_failed[@]}" > "$SOFT_LOG"; exit "$status"' \
|
|
bash "$function_name" "${function_args[@]}"
|
|
) > "$case_root/output" 2>&1
|
|
}
|
|
|
|
assert_log() {
|
|
local name="$1" expected="$2" path
|
|
path="$test_tmp/cases/$name/commands.log"
|
|
[[ "$(<"$path")" == "$expected" ]] || {
|
|
printf 'package provenance contract: unexpected %s command log\n' "$name" >&2
|
|
diff -u <(printf '%s\n' "$expected") "$path" >&2 || true
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
assert_soft_failure() {
|
|
local name="$1" component="$2"
|
|
[[ "$(<"$test_tmp/cases/$name/softly-failed")" == "$component" ]] \
|
|
|| fail "$name did not record exactly one $component soft failure"
|
|
}
|
|
|
|
assert_root_snapshot_logged() {
|
|
local name="$1" file="$2"
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" == *"sudo:root-stage:$file"* \
|
|
&& "$(<"$test_tmp/cases/$name/commands.log")" == *"sudo:root-verify:$file"* ]] \
|
|
|| fail "$name did not reverify privileged snapshot $file"
|
|
}
|
|
|
|
assert_user_source_swapped() {
|
|
local name="$1"
|
|
[[ -s "$test_tmp/cases/$name/swap-marker" ]] \
|
|
|| fail "$name did not exercise the post-snapshot source replacement adapter"
|
|
}
|
|
|
|
assert_no_download() {
|
|
local name="$1"
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'curl:'* ]] \
|
|
|| fail "$name reached curl"
|
|
}
|
|
|
|
assert_no_runtime_staging() {
|
|
local name="$1"
|
|
[[ -z "$(find "$test_tmp/cases/$name/home" \
|
|
\( -name '*.part.*' -o -name '*.stage.*' -o -name '*.link.*' \) -print -quit)" \
|
|
&& -z "$(find "$test_tmp/cases/$name/tmp" -mindepth 1 -print -quit)" \
|
|
&& -z "$(find "$test_tmp/cases/$name/root-staging" -mindepth 1 -print -quit)" ]] \
|
|
|| fail "$name left private runtime staging behind"
|
|
}
|
|
|
|
assert_old_runtime_preserved() {
|
|
local name="$1" component="$2" home="$test_tmp/cases/$name/home"
|
|
case "$component" in
|
|
Node)
|
|
[[ "$($home/.nvm/versions/node/v23.0.0/bin/node --version)" == v23.0.0 ]] \
|
|
|| fail "$name changed the known-good Node"
|
|
;;
|
|
Bun)
|
|
[[ "$(readlink "$home/.bun/bin/bun")" == \
|
|
"$home/.bun/versions/1.3.0/bin/bun" ]] \
|
|
|| fail "$name changed the active Bun link"
|
|
[[ "$($home/.bun/bin/bun --version)" == 1.3.0 ]] \
|
|
|| fail "$name changed the known-good Bun"
|
|
;;
|
|
Codex)
|
|
[[ "$(readlink "$home/.local/bin/codex")" == \
|
|
"$home/.local/lib/panama/codex/0.149.0/codex" ]] \
|
|
|| fail "$name changed the active Codex link"
|
|
[[ "$($home/.local/bin/codex --version)" == 'codex-cli 0.149.0' ]] \
|
|
|| fail "$name changed the known-good Codex"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Each supported architecture selects its own reviewed URL, digest and archive
|
|
# layout. Successful activation leaves no private download or extraction tree.
|
|
for runtime_case in \
|
|
'node-x86_64 x86_64 install_node Node https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz 67108864' \
|
|
'node-aarch64 aarch64 install_node Node https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz 67108864' \
|
|
'bun-x86_64 x86_64 install_bun Bun https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip 67108864' \
|
|
'bun-aarch64 aarch64 install_bun Bun https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip 67108864' \
|
|
'codex-x86_64 x86_64 install_codex Codex https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz 134217728' \
|
|
'codex-aarch64 aarch64 install_codex Codex https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz 134217728'; do
|
|
read -r name arch function_name component url max_bytes <<<"$runtime_case"
|
|
reset_installer_fixture
|
|
if ! STUB_ARCH="$arch" run_installer_function "$name" "$function_name"; then
|
|
tail -n 120 "$test_tmp/cases/$name/output" >&2
|
|
fail "expected successful $name activation"
|
|
fi
|
|
grep -qFx "curl:$url:max=$max_bytes:output=artifact" \
|
|
"$test_tmp/cases/$name/commands.log" \
|
|
|| { sed -n '1,80p' "$test_tmp/cases/$name/output" >&2; sed -n '1,80p' "$test_tmp/cases/$name/commands.log" >&2; fail "$name did not select its reviewed artifact"; }
|
|
[[ ! -s "$test_tmp/cases/$name/softly-failed" ]] \
|
|
|| fail "$name recorded a soft failure after successful activation"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
for receipt_spec in \
|
|
'node-x86_64 .nvm/versions/node/v24.20.0 2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2 89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7' \
|
|
'node-aarch64 .nvm/versions/node/v24.20.0 5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7 23a5637c2470fde09fcc1acc77c1b92e04e3d7e3e6e80ff7df6f5831958d1477' \
|
|
'bun-x86_64 .bun/versions/1.4.0 2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452 33d56b070be6a9e3da0ab013038b43d1645d0534ca811ecdba4472599117eb4b' \
|
|
'bun-aarch64 .bun/versions/1.4.0 4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e 086c4121c8738a8e0f5ed730e8a461bc3973b4444e372ddb77aef9a747fa2ae9' \
|
|
'codex-x86_64 .local/lib/panama/codex/0.150.1 00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17 abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386' \
|
|
'codex-aarch64 .local/lib/panama/codex/0.150.1 1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639 7a49aabe11fd95a1c968d79e16b5f1b17c3219002c5f7129d467f415f8460feb'; do
|
|
read -r name target_relative artifact_digest binary_digest <<<"$receipt_spec"
|
|
assert_file_bytes "$test_tmp/cases/$name/home/$target_relative/.panama-provenance" \
|
|
"$(printf 'schema=1\nartifact_sha256=%s\nbinary_sha256=%s\n' \
|
|
"$artifact_digest" "$binary_digest")"
|
|
done
|
|
|
|
# No-op trust comes only from directories produced and attested by a successful
|
|
# installer run, never from a handcrafted executable that prints the version.
|
|
for exact_spec in \
|
|
'node-x86_64 x86_64 install_node' \
|
|
'node-aarch64 aarch64 install_node' \
|
|
'bun-x86_64 x86_64 install_bun' \
|
|
'bun-aarch64 aarch64 install_bun' \
|
|
'codex-x86_64 x86_64 install_codex' \
|
|
'codex-aarch64 aarch64 install_codex'; do
|
|
read -r name arch function_name <<<"$exact_spec"
|
|
reset_installer_fixture
|
|
STUB_REUSE_CASE=1 STUB_ARCH="$arch" expect_success \
|
|
run_installer_function "$name" "$function_name"
|
|
assert_no_download "$name"
|
|
[[ ! -s "$test_tmp/cases/$name/softly-failed" ]] \
|
|
|| fail "$name rejected its installer-produced provenance receipt"
|
|
done
|
|
|
|
[[ "$($test_tmp/cases/node-x86_64/home/.nvm/versions/node/v24.20.0/bin/node --version)" \
|
|
== v24.20.0 ]] || fail 'x86_64 Node activation has the wrong version'
|
|
grep -qFx 'nvm:alias default 24.20.0' "$test_tmp/cases/node-x86_64/commands.log" \
|
|
|| fail 'Node did not set the exact nvm default alias'
|
|
[[ "$($test_tmp/cases/bun-x86_64/home/.bun/bin/bun --version)" == 1.4.0 ]] \
|
|
|| fail 'x86_64 Bun activation has the wrong version'
|
|
[[ "$(readlink "$test_tmp/cases/bun-x86_64/home/.bun/bin/bun")" == \
|
|
"$test_tmp/cases/bun-x86_64/home/.bun/versions/1.4.0/bin/bun" ]] \
|
|
|| fail 'Bun did not atomically activate the reviewed version path'
|
|
[[ "$($test_tmp/cases/codex-x86_64/home/.local/bin/codex --version)" == \
|
|
'codex-cli 0.150.1' ]] || fail 'x86_64 Codex activation has the wrong version'
|
|
[[ "$(readlink "$test_tmp/cases/codex-x86_64/home/.local/bin/codex")" == \
|
|
"$test_tmp/cases/codex-x86_64/home/.local/lib/panama/codex/0.150.1/codex" ]] \
|
|
|| fail 'Codex did not atomically activate the reviewed version path'
|
|
|
|
# Unsupported CPUs stop before curl. RustDesk's reviewed RPM is x86_64-only,
|
|
# so aarch64 is also an intentional, recorded soft failure without a download.
|
|
for unsupported_case in \
|
|
'node-unsupported install_node Node riscv64' \
|
|
'bun-unsupported install_bun Bun riscv64' \
|
|
'codex-unsupported install_codex Codex riscv64' \
|
|
'rustdesk-unsupported install_rustdesk RustDesk riscv64' \
|
|
'rustdesk-aarch64 install_rustdesk RustDesk aarch64'; do
|
|
read -r name function_name component arch <<<"$unsupported_case"
|
|
reset_installer_fixture
|
|
STUB_ARCH="$arch" expect_failure run_installer_function "$name" "$function_name"
|
|
assert_no_download "$name"
|
|
assert_soft_failure "$name" "$component"
|
|
done
|
|
|
|
# A target can appear after the initial absence check. Activation must not
|
|
# replace it or move the staged directory inside it, and active older tools
|
|
# must stay selected.
|
|
for component_spec in \
|
|
'Node install_node .nvm/versions/node/v24.20.0' \
|
|
'Bun install_bun .bun/versions/1.4.0' \
|
|
'Codex install_codex .local/lib/panama/codex/0.150.1'; do
|
|
read -r component function_name target_relative <<<"$component_spec"
|
|
name="${component,,}-late-collision"
|
|
reset_installer_fixture
|
|
STUB_SEED_OLD="$component" STUB_LATE_COLLISION="$component" \
|
|
expect_failure run_installer_function "$name" "$function_name"
|
|
target="$test_tmp/cases/$name/home/$target_relative"
|
|
assert_file_bytes "$target/collision-marker" 'preserved collision'
|
|
[[ -z "$(find "$target" -mindepth 1 ! -name collision-marker -print -quit)" ]] \
|
|
|| fail "$name nested verified staging into the late collision"
|
|
assert_old_runtime_preserved "$name" "$component"
|
|
assert_soft_failure "$name" "$component"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# A staged Node must prove that links stay inside the verified tree before its
|
|
# executable can run. The hard-link adapter changes the extracted inode at the
|
|
# filesystem boundary, which catches checks that inspect tar names only.
|
|
for escape_kind in symlink hardlink; do
|
|
name="node-$escape_kind-escape"
|
|
reset_installer_fixture
|
|
STUB_NODE_ESCAPE="$escape_kind" expect_failure \
|
|
run_installer_function "$name" install_node
|
|
[[ ! -e "$test_tmp/cases/$name/outside-executed" ]] \
|
|
|| fail "$name executed a Node target outside the staged tree"
|
|
assert_soft_failure "$name" Node
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# A traversal command that cannot read/describe the staged tree is itself a
|
|
# trust failure. Node traverses an unreadable subtree and Codex emits malformed
|
|
# output before failing; scanners that lose the status execute the outside tool.
|
|
for traversal_spec in 'Node install_node' 'Codex install_codex'; do
|
|
read -r component function_name <<<"$traversal_spec"
|
|
name="${component,,}-traversal-error"
|
|
reset_installer_fixture
|
|
STUB_TRAVERSAL_ERROR="$component" expect_failure \
|
|
run_installer_function "$name" "$function_name"
|
|
grep -qFx "find:traversal-error:$component" \
|
|
"$test_tmp/cases/$name/commands.log" \
|
|
|| fail "$name did not exercise the traversal error"
|
|
[[ ! -e "$test_tmp/cases/$name/outside-executed" ]] \
|
|
|| fail "$name executed a target hidden by a failed traversal"
|
|
assert_soft_failure "$name" "$component"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# A bad digest or interrupted transfer cannot replace the previously active
|
|
# tool and cannot leave reusable bytes behind.
|
|
for failure_mode in digest interrupted; do
|
|
for component_spec in \
|
|
'Node install_node' \
|
|
'Bun install_bun' \
|
|
'Codex install_codex'; do
|
|
read -r component function_name <<<"$component_spec"
|
|
name="${component,,}-$failure_mode"
|
|
reset_installer_fixture
|
|
if [[ "$failure_mode" == digest ]]; then
|
|
STUB_SEED_OLD="$component" STUB_DIGEST_MISMATCH=1 \
|
|
expect_failure run_installer_function "$name" "$function_name"
|
|
else
|
|
STUB_SEED_OLD="$component" STUB_DOWNLOAD_INTERRUPT=1 \
|
|
expect_failure run_installer_function "$name" "$function_name"
|
|
fi
|
|
assert_soft_failure "$name" "$component"
|
|
assert_old_runtime_preserved "$name" "$component"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
name="rustdesk-$failure_mode"
|
|
reset_installer_fixture
|
|
if [[ "$failure_mode" == digest ]]; then
|
|
STUB_RUSTDESK_VERSION=1.4.8 STUB_DIGEST_MISMATCH=1 \
|
|
expect_failure run_installer_function "$name" install_rustdesk
|
|
else
|
|
STUB_RUSTDESK_VERSION=1.4.8 STUB_DOWNLOAD_INTERRUPT=1 \
|
|
expect_failure run_installer_function "$name" install_rustdesk
|
|
fi
|
|
assert_soft_failure "$name" RustDesk
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:'* ]] \
|
|
|| fail "$name reached DNF with an unverified RPM"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# Deliver a real signal to each isolated installer process group while private
|
|
# state exists. Cleanup must run for download, extraction, directory activation,
|
|
# and active-link replacement without changing an older selected runtime.
|
|
for signal_spec in \
|
|
'node-signal-extract install_node Node extract' \
|
|
'bun-signal-activation install_bun Bun activation' \
|
|
'codex-signal-download install_codex Codex download' \
|
|
'rustdesk-signal-download install_rustdesk RustDesk download' \
|
|
'bun-signal-link install_bun Bun link' \
|
|
'codex-signal-link install_codex Codex link'; do
|
|
read -r name function_name component phase <<<"$signal_spec"
|
|
reset_installer_fixture
|
|
if [[ "$component" == RustDesk ]]; then
|
|
STUB_RUSTDESK_VERSION=1.4.8 STUB_SIGNAL_PHASE="$phase" \
|
|
expect_failure run_installer_function "$name" "$function_name" 2>/dev/null
|
|
else
|
|
STUB_SEED_OLD="$component" STUB_SIGNAL_PHASE="$phase" \
|
|
expect_failure run_installer_function "$name" "$function_name" 2>/dev/null
|
|
assert_old_runtime_preserved "$name" "$component"
|
|
fi
|
|
grep -qFx "signal:$phase" "$test_tmp/cases/$name/commands.log" \
|
|
|| fail "$name did not deliver its real process-group signal"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# RPM key verification owns a private RPM database. A real signal delivered
|
|
# after that database exists must remove it before the isolated process exits.
|
|
reset_installer_fixture
|
|
STUB_SIGNAL_PHASE=rpmdb expect_failure run_installer_function rpmdb-signal \
|
|
rpm_signature_matches "$test_tmp/signed-fixture.rpm" \
|
|
"$installer_fixture/setup/provenance/keys/rpmfusion-free.asc" \
|
|
E9A491A3DE247814E7E067EAE06F8ECDD651FF2E 2>/dev/null
|
|
grep -qFx 'signal:rpmdb' "$test_tmp/cases/rpmdb-signal/commands.log" \
|
|
|| fail 'rpmdb-signal did not deliver its real process-group signal'
|
|
assert_no_runtime_staging rpmdb-signal
|
|
|
|
reset_installer_fixture
|
|
STUB_SIGNAL_PHASE=gpg-home expect_failure run_installer_function gpg-home-signal \
|
|
key_fingerprint_matches \
|
|
"$installer_fixture/setup/provenance/keys/rpmfusion-free.asc" \
|
|
E9A491A3DE247814E7E067EAE06F8ECDD651FF2E 2>/dev/null
|
|
grep -qFx 'signal:gpg-home' "$test_tmp/cases/gpg-home-signal/commands.log" \
|
|
|| fail 'gpg-home-signal did not deliver its real process-group signal'
|
|
assert_no_runtime_staging gpg-home-signal
|
|
|
|
# Root staging owns cleanup before the privileged directory exists and until
|
|
# reviewed snapshots have been returned to their caller. Signals in either
|
|
# window must not strand a root-owned panama-install directory.
|
|
reset_installer_fixture
|
|
STUB_RUSTDESK_VERSION=1.4.8 STUB_SIGNAL_PHASE=root-create \
|
|
expect_failure run_installer_function root-create-signal install_rustdesk 2>/dev/null
|
|
grep -qFx 'signal:root-create' "$test_tmp/cases/root-create-signal/commands.log" \
|
|
|| fail 'root-create-signal did not deliver its real process-group signal'
|
|
assert_no_runtime_staging root-create-signal
|
|
|
|
reset_installer_fixture
|
|
STUB_SIGNAL_PHASE=root-review \
|
|
expect_failure run_installer_function root-review-signal install_rpmfusion_repositories 2>/dev/null
|
|
grep -qFx 'signal:root-review' "$test_tmp/cases/root-review-signal/commands.log" \
|
|
|| fail 'root-review-signal did not deliver its real process-group signal'
|
|
assert_no_runtime_staging root-review-signal
|
|
|
|
reset_installer_fixture
|
|
STUB_SIGNAL_PHASE=repo-work \
|
|
expect_failure run_installer_function repo-work-signal \
|
|
install_rpmfusion_repositories 2>/dev/null
|
|
grep -qFx 'signal:repo-work' "$test_tmp/cases/repo-work-signal/commands.log" \
|
|
|| fail 'repo-work-signal did not deliver its real process-group signal'
|
|
assert_no_runtime_staging repo-work-signal
|
|
|
|
# If privileged cleanup itself fails, do not erase the only recovery evidence.
|
|
# The caller returns failure and reports the exact retained private directory.
|
|
reset_installer_fixture
|
|
STUB_RUSTDESK_VERSION=1.4.8 STUB_ROOT_CLEANUP_FAIL=1 \
|
|
expect_failure run_installer_function root-cleanup-failure install_rustdesk
|
|
grep -qF 'Installer staging cleanup failed. Retained artifact:' \
|
|
"$test_tmp/cases/root-cleanup-failure/output" \
|
|
|| fail 'root cleanup failure did not report retained evidence'
|
|
[[ -n "$(find "$test_tmp/cases/root-cleanup-failure/root-staging" \
|
|
-mindepth 1 -print -quit)" ]] \
|
|
|| fail 'root cleanup failure discarded its reported recovery evidence'
|
|
|
|
# Successful updates keep the old version directory and switch only the active
|
|
# symlink after the replacement binary has passed its version check.
|
|
for component_spec in 'Bun install_bun .bun/bin/bun .bun/versions/1.4.0/bin/bun' \
|
|
'Codex install_codex .local/bin/codex .local/lib/panama/codex/0.150.1/codex'; do
|
|
read -r component function_name active_relative target_relative <<<"$component_spec"
|
|
name="${component,,}-atomic-update"
|
|
reset_installer_fixture
|
|
STUB_SEED_OLD="$component" expect_success run_installer_function "$name" "$function_name"
|
|
[[ "$(readlink "$test_tmp/cases/$name/home/$active_relative")" == \
|
|
"$test_tmp/cases/$name/home/$target_relative" ]] \
|
|
|| fail "$name did not atomically replace the active symlink"
|
|
if [[ "$component" == Bun ]]; then
|
|
[[ "$($test_tmp/cases/$name/home/.bun/versions/1.3.0/bin/bun --version)" == 1.3.0 ]] \
|
|
|| fail "$name removed the prior version directory"
|
|
else
|
|
[[ "$($test_tmp/cases/$name/home/.local/lib/panama/codex/0.149.0/codex --version)" \
|
|
== 'codex-cli 0.149.0' ]] || fail "$name removed the prior version directory"
|
|
fi
|
|
done
|
|
|
|
# Receipt or executable tampering preserves the directory and leaves an older
|
|
# active link unchanged. A matching version string is not an attestation.
|
|
for tamper_spec in \
|
|
'Node install_node .nvm/versions/node/v24.20.0 bin/node v24.20.0' \
|
|
'Bun install_bun .bun/versions/1.4.0 bin/bun 1.4.0' \
|
|
'Codex install_codex .local/lib/panama/codex/0.150.1 codex codex-cli_0.150.1'; do
|
|
read -r component function_name target_relative binary_relative version_text <<<"$tamper_spec"
|
|
for tamper_kind in receipt binary; do
|
|
name="${component,,}-$tamper_kind-tamper"
|
|
reset_installer_fixture
|
|
STUB_SEED_OLD="$component" expect_success \
|
|
run_installer_function "$name" "$function_name"
|
|
home="$test_tmp/cases/$name/home"
|
|
target="$home/$target_relative"
|
|
if [[ "$component" == Bun ]]; then
|
|
ln -sfn "$home/.bun/versions/1.3.0/bin/bun" "$home/.bun/bin/bun"
|
|
elif [[ "$component" == Codex ]]; then
|
|
ln -sfn "$home/.local/lib/panama/codex/0.149.0/codex" "$home/.local/bin/codex"
|
|
fi
|
|
if [[ "$tamper_kind" == receipt ]]; then
|
|
sed -i 's/^artifact_sha256=.*/artifact_sha256=0000000000000000000000000000000000000000000000000000000000000000/' \
|
|
"$target/.panama-provenance"
|
|
else
|
|
version_text="${version_text//_/ }"
|
|
printf '#!/usr/bin/env bash\nprintf "%s\\n"\n' "$version_text" \
|
|
> "$target/$binary_relative"
|
|
chmod +x "$target/$binary_relative"
|
|
fi
|
|
STUB_REUSE_CASE=1 expect_failure \
|
|
run_installer_function "$name" "$function_name"
|
|
assert_no_download "$name"
|
|
assert_soft_failure "$name" "$component"
|
|
if [[ "$component" != Node ]]; then
|
|
assert_old_runtime_preserved "$name" "$component"
|
|
fi
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
done
|
|
|
|
# A reviewed digest does not excuse a malformed archive. Reject the wrong top
|
|
# level or any extra member before a version path or active link appears.
|
|
for layout_case in \
|
|
'node-layout install_node Node .nvm/versions/node/v24.20.0' \
|
|
'bun-layout install_bun Bun .bun/versions/1.4.0' \
|
|
'codex-layout install_codex Codex .local/lib/panama/codex/0.150.1'; do
|
|
read -r name function_name component relative_target <<<"$layout_case"
|
|
reset_installer_fixture
|
|
STUB_BAD_LAYOUT=1 expect_failure run_installer_function "$name" "$function_name"
|
|
assert_soft_failure "$name" "$component"
|
|
[[ ! -e "$test_tmp/cases/$name/home/$relative_target" ]] \
|
|
|| fail "$name activated an archive with an unexpected layout"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# ZIP names alone do not establish entry type. The extraction adapter
|
|
# deliberately materializes all crafted entries as regular executable files,
|
|
# so only central-directory attribute validation can reject them pre-extraction.
|
|
for attribute_kind in symlink special directory; do
|
|
name="bun-$attribute_kind-attribute"
|
|
reset_installer_fixture
|
|
STUB_BUN_ATTRIBUTE="$attribute_kind" expect_failure \
|
|
run_installer_function "$name" install_bun
|
|
assert_soft_failure "$name" Bun
|
|
[[ ! -e "$test_tmp/cases/$name/home/.bun/versions/1.4.0" ]] \
|
|
|| fail "$name activated a ZIP entry with non-regular metadata"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# Legacy version-only and mismatched collisions are preserved and reported;
|
|
# neither can be deleted and recreated or treated as installer provenance.
|
|
for collision_mode in legacy collision; do
|
|
for component_spec in \
|
|
'Node install_node' \
|
|
'Bun install_bun' \
|
|
'Codex install_codex'; do
|
|
read -r component function_name <<<"$component_spec"
|
|
name="${component,,}-$collision_mode"
|
|
reset_installer_fixture
|
|
if [[ "$collision_mode" == legacy ]]; then
|
|
STUB_SEED_LEGACY="$component" expect_failure \
|
|
run_installer_function "$name" "$function_name"
|
|
assert_soft_failure "$name" "$component"
|
|
else
|
|
STUB_SEED_COLLISION="$component" expect_failure \
|
|
run_installer_function "$name" "$function_name"
|
|
assert_soft_failure "$name" "$component"
|
|
fi
|
|
assert_no_download "$name"
|
|
done
|
|
done
|
|
|
|
reset_installer_fixture
|
|
STUB_ARCH=x86_64 STUB_RUSTDESK_VERSION=1.4.8 \
|
|
expect_success run_installer_function rustdesk-x86_64 install_rustdesk
|
|
assert_log rustdesk-x86_64 "$(cat <<'EXPECTED'
|
|
rpm:query:rustdesk
|
|
curl:https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm:max=134217728:output=rustdesk.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rustdesk.rpm
|
|
sudo:root-verify:rustdesk.rpm
|
|
sudo:dnf install -y --repo=fedora --repo=updates --setopt=localpkg_gpgcheck=0 RUSTDESK_LOCAL
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
[[ "$(<"$test_tmp/cases/rustdesk-x86_64/commands.log")" == *'sudo:root-stage:rustdesk.rpm'* ]] \
|
|
|| fail 'RustDesk did not cross a reverified private privileged snapshot'
|
|
assert_no_runtime_staging rustdesk-x86_64
|
|
|
|
reset_installer_fixture
|
|
STUB_ARCH=x86_64 STUB_RUSTDESK_VERSION=1.4.8 \
|
|
STUB_SWAP_AFTER_ROOT_STAGE=rustdesk.rpm \
|
|
expect_success run_installer_function rustdesk-root-bound install_rustdesk
|
|
assert_user_source_swapped rustdesk-root-bound
|
|
assert_no_runtime_staging rustdesk-root-bound
|
|
|
|
reset_installer_fixture
|
|
STUB_ARCH=x86_64 STUB_RUSTDESK_VERSION=1.4.9 \
|
|
expect_success run_installer_function rustdesk-exact install_rustdesk
|
|
assert_log rustdesk-exact 'rpm:query:rustdesk'
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function pnpm install_pnpm
|
|
assert_log pnpm "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
sudo:dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates pnpm
|
|
EXPECTED
|
|
)"
|
|
|
|
reset_installer_fixture
|
|
mkdir -p "$installer_fixture/setup/packages"
|
|
printf 'fixture-package\n' >"$installer_fixture/setup/packages/base-fixture"
|
|
expect_success run_installer_function base-list install_list base-fixture Base
|
|
assert_log base-list "$(cat <<'EXPECTED'
|
|
sudo:dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates --skip-unavailable fixture-package
|
|
rpm:query:fixture-package
|
|
EXPECTED
|
|
)"
|
|
|
|
# Mixed package lists are split before DNF sees them. Publisher-exclusive names
|
|
# are singularly bound to that publisher, while ordinary Fedora/RPM Fusion
|
|
# packages never admit Terra, the Hyprland COPR, or Cisco as alternate sources.
|
|
reset_installer_fixture
|
|
cat > "$installer_fixture/setup/packages/desktop-source-fixture" <<'FIXTURE'
|
|
NetworkManager
|
|
cascadiamono-nerd-fonts
|
|
espanso-wayland
|
|
firamono-nerd-fonts
|
|
ghostty
|
|
jetbrainsmono-nerd-fonts
|
|
nautilus-open-any-terminal
|
|
victormono-nerd-fonts
|
|
gstreamer1-plugin-openh264
|
|
mozilla-openh264
|
|
FIXTURE
|
|
STUB_OPENH264_REPO=1 expect_success run_installer_function desktop-source-split \
|
|
install_desktop_package_file \
|
|
"$installer_fixture/setup/packages/desktop-source-fixture"
|
|
assert_log desktop-source-split "$(cat <<'EXPECTED'
|
|
sudo:dnf install -y --repo=fedora --repo=updates --repo=rpmfusion-free --repo=rpmfusion-free-updates --repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates --repo=fedora-cisco-openh264 --from-repo=fedora,updates --skip-unavailable NetworkManager gstreamer1-plugin-openh264
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:panama-bound-terra.asc
|
|
sudo:root-verify:panama-bound-terra.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
sudo:root-private
|
|
sudo:dnf install -y --repofrompath panama-bound-terra,https://repos.fyralabs.com/terra44 --repo=panama-bound-terra --repo=fedora --repo=updates --from-repo=panama-bound-terra --setopt=panama-bound-terra.gpgcheck=1 --setopt=panama-bound-terra.repo_gpgcheck=1 --setopt=panama-bound-terra.gpgkey=file://BOUND_KEY --skip-unavailable cascadiamono-nerd-fonts espanso-wayland firamono-nerd-fonts ghostty jetbrainsmono-nerd-fonts nautilus-open-any-terminal victormono-nerd-fonts
|
|
sudo:root-cleanup
|
|
sudo:dnf install -y --repo=fedora --repo=updates --repo=fedora-cisco-openh264 --from-repo=fedora-cisco-openh264 --skip-unavailable mozilla-openh264
|
|
rpm:query:NetworkManager
|
|
rpm:query:cascadiamono-nerd-fonts
|
|
rpm:query:espanso-wayland
|
|
rpm:query:firamono-nerd-fonts
|
|
rpm:query:ghostty
|
|
rpm:query:jetbrainsmono-nerd-fonts
|
|
rpm:query:nautilus-open-any-terminal
|
|
rpm:query:victormono-nerd-fonts
|
|
rpm:query:gstreamer1-plugin-openh264
|
|
rpm:query:mozilla-openh264
|
|
EXPECTED
|
|
)"
|
|
|
|
reset_installer_fixture
|
|
cat > "$installer_fixture/setup/packages/hyprland-source-fixture" <<'FIXTURE'
|
|
NetworkManager
|
|
gpu-screen-recorder
|
|
grimblast
|
|
helium-browser-bin
|
|
hypridle
|
|
hyprland
|
|
hyprland-guiutils
|
|
hyprland-uwsm
|
|
hyprlock
|
|
hyprpaper
|
|
hyprpicker
|
|
hyprpolkitagent
|
|
hyprpwcenter
|
|
hyprshutdown
|
|
hyprsunset
|
|
hyprsysteminfo
|
|
mpvpaper
|
|
quickshell
|
|
satty
|
|
uwsm
|
|
vicinae
|
|
xdg-desktop-portal-hyprland
|
|
FIXTURE
|
|
expect_success run_installer_function hyprland-source-split \
|
|
install_hyprland_package_file \
|
|
"$installer_fixture/setup/packages/hyprland-source-fixture"
|
|
assert_log hyprland-source-split "$(cat <<'EXPECTED'
|
|
sudo:dnf install -y --repo=fedora --repo=updates --repo=rpmfusion-free --repo=rpmfusion-free-updates --repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates --from-repo=fedora,updates --setopt=install_weak_deps=False NetworkManager
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:panama-bound-hyprland.asc
|
|
sudo:root-verify:panama-bound-hyprland.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:97E23476C89635135407C7D5E9BA41342C4B2995
|
|
sudo:root-private
|
|
sudo:dnf install -y --repofrompath panama-bound-hyprland,https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/ --repo=panama-bound-hyprland --repo=fedora --repo=updates --from-repo=panama-bound-hyprland --setopt=panama-bound-hyprland.gpgcheck=1 --setopt=panama-bound-hyprland.repo_gpgcheck=0 --setopt=panama-bound-hyprland.gpgkey=file://BOUND_KEY --setopt=install_weak_deps=False gpu-screen-recorder grimblast hypridle hyprland hyprland-guiutils hyprland-uwsm hyprlock hyprpaper hyprpicker hyprpolkitagent hyprpwcenter hyprshutdown hyprsunset hyprsysteminfo quickshell uwsm xdg-desktop-portal-hyprland
|
|
sudo:root-cleanup
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:panama-bound-terra.asc
|
|
sudo:root-verify:panama-bound-terra.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
sudo:root-private
|
|
sudo:dnf install -y --repofrompath panama-bound-terra,https://repos.fyralabs.com/terra44 --repo=panama-bound-terra --repo=fedora --repo=updates --from-repo=panama-bound-terra --setopt=panama-bound-terra.gpgcheck=1 --setopt=panama-bound-terra.repo_gpgcheck=1 --setopt=panama-bound-terra.gpgkey=file://BOUND_KEY --setopt=install_weak_deps=False helium-browser-bin mpvpaper satty vicinae
|
|
sudo:root-cleanup
|
|
rpm:query:NetworkManager
|
|
rpm:query:gpu-screen-recorder
|
|
rpm:query:grimblast
|
|
rpm:query:helium-browser-bin
|
|
rpm:query:hypridle
|
|
rpm:query:hyprland
|
|
rpm:query:hyprland-guiutils
|
|
rpm:query:hyprland-uwsm
|
|
rpm:query:hyprlock
|
|
rpm:query:hyprpaper
|
|
rpm:query:hyprpicker
|
|
rpm:query:hyprpolkitagent
|
|
rpm:query:hyprpwcenter
|
|
rpm:query:hyprshutdown
|
|
rpm:query:hyprsunset
|
|
rpm:query:hyprsysteminfo
|
|
rpm:query:mpvpaper
|
|
rpm:query:quickshell
|
|
rpm:query:satty
|
|
rpm:query:uwsm
|
|
rpm:query:vicinae
|
|
rpm:query:xdg-desktop-portal-hyprland
|
|
EXPECTED
|
|
)"
|
|
for bound_repo in panama-bound-hyprland panama-bound-terra; do
|
|
grep -q -- "--repofrompath $bound_repo," \
|
|
"$test_tmp/cases/hyprland-source-split/commands.log" \
|
|
|| fail "Hyprland source split did not bind $bound_repo to its reviewed URL"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
STUB_DNF_FAIL_MATCH=pnpm expect_failure run_installer_function pnpm-failure install_pnpm
|
|
assert_soft_failure pnpm-failure pnpm
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function rpmfusion install_rpmfusion_repositories
|
|
assert_log rpmfusion "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
curl:https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm:max=4194304:output=rpmfusion-free-release.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rpmfusion-free-release.rpm
|
|
sudo:root-verify:rpmfusion-free-release.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rpmfusion-free.asc
|
|
sudo:root-verify:rpmfusion-free.asc
|
|
sudo:root-reviewable
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:E9A491A3DE247814E7E067EAE06F8ECDD651FF2E
|
|
rpmkeys:import:rpmfusion-free.asc
|
|
rpmkeys:checksig:rpmfusion-free-release.rpm
|
|
sudo:root-private
|
|
sudo:root-private
|
|
curl:https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm:max=4194304:output=rpmfusion-nonfree-release.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rpmfusion-nonfree-release.rpm
|
|
sudo:root-verify:rpmfusion-nonfree-release.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rpmfusion-nonfree.asc
|
|
sudo:root-verify:rpmfusion-nonfree.asc
|
|
sudo:root-reviewable
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:79BDB88F9BBF73910FD4095B6A2AF96194843C65
|
|
rpmkeys:import:rpmfusion-nonfree.asc
|
|
rpmkeys:checksig:rpmfusion-nonfree-release.rpm
|
|
sudo:root-private
|
|
sudo:root-private
|
|
sudo:rpm-import:rpmfusion-free.asc
|
|
sudo:rpm-import:rpmfusion-nonfree.asc
|
|
sudo:dnf install -y --repo=fedora --repo=updates --setopt=localpkg_gpgcheck=1 RPMFUSION_FREE RPMFUSION_NONFREE
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
assert_root_snapshot_logged rpmfusion rpmfusion-free-release.rpm
|
|
assert_root_snapshot_logged rpmfusion rpmfusion-nonfree-release.rpm
|
|
|
|
# DNF checks command-line RPMs against the system RPM keyring, not the private
|
|
# verification database. A fresh Fedora keyring therefore needs the two
|
|
# already-reviewed root snapshots imported before localpkg_gpgcheck runs.
|
|
reset_installer_fixture
|
|
STUB_REQUIRE_RPMFUSION_SYSTEM_KEYS=1 \
|
|
expect_success run_installer_function rpmfusion-fresh-keyring \
|
|
install_rpmfusion_repositories
|
|
|
|
reset_installer_fixture
|
|
STUB_SWAP_AFTER_ROOT_STAGE=rpmfusion-free-release.rpm \
|
|
expect_success run_installer_function rpmfusion-root-bound install_rpmfusion_repositories
|
|
assert_user_source_swapped rpmfusion-root-bound
|
|
assert_no_runtime_staging rpmfusion-root-bound
|
|
|
|
reset_installer_fixture
|
|
STUB_TOGGLE_KEY_VERIFY=rpmfusion-free.asc \
|
|
expect_failure run_installer_function rpmfusion-key-toggle install_rpmfusion_repositories
|
|
assert_user_source_swapped rpmfusion-key-toggle
|
|
[[ "$(<"$test_tmp/cases/rpmfusion-key-toggle/commands.log")" != *'sudo:dnf'* ]] \
|
|
|| fail 'same-UID RPM Fusion key toggle reached package activation'
|
|
assert_no_runtime_staging rpmfusion-key-toggle
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function terra install_terra_repository
|
|
assert_log terra "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
dnf:dump-all:locale=C
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:terra44.asc
|
|
sudo:root-verify:terra44.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
sudo:root-private
|
|
sudo:root-verify:terra44.asc
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:terra.repo
|
|
sudo:root-verify:terra.repo
|
|
sudo:install:terra44.asc:/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama
|
|
sudo:install:terra.repo:/etc/yum.repos.d/terra.repo
|
|
dnf:dump-all:locale=C
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
cmp -s "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$test_tmp/cases/terra/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama" \
|
|
|| fail 'Terra privileged install did not preserve the fully staged reviewed key'
|
|
assert_file_bytes "$test_tmp/cases/terra/etc/yum.repos.d/terra.repo" "$(cat <<'EXPECTED'
|
|
[terra]
|
|
name=Panama reviewed Terra 44
|
|
baseurl=https://repos.fyralabs.com/terra44
|
|
enabled=1
|
|
gpgcheck=1
|
|
repo_gpgcheck=1
|
|
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama
|
|
EXPECTED
|
|
)"
|
|
assert_root_snapshot_logged terra terra44.asc
|
|
assert_root_snapshot_logged terra terra.repo
|
|
|
|
for terra_snapshot in terra44.asc terra.repo; do
|
|
reset_installer_fixture
|
|
name="terra-root-bound-${terra_snapshot%.*}"
|
|
STUB_SWAP_AFTER_ROOT_STAGE="$terra_snapshot" \
|
|
expect_success run_installer_function "$name" install_terra_repository
|
|
assert_user_source_swapped "$name"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function hyprland configure_hyprland_repository
|
|
assert_log hyprland "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:hyprland-copr.asc
|
|
sudo:root-verify:hyprland-copr.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:97E23476C89635135407C7D5E9BA41342C4B2995
|
|
sudo:root-private
|
|
sudo:root-verify:hyprland-copr.asc
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:panama-hyprland.repo
|
|
sudo:root-verify:panama-hyprland.repo
|
|
sudo:install:hyprland-copr.asc:/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland
|
|
sudo:install:panama-hyprland.repo:/etc/yum.repos.d/panama-hyprland.repo
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
cmp -s "$installer_fixture/setup/provenance/keys/hyprland-copr.asc" \
|
|
"$test_tmp/cases/hyprland/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland" \
|
|
|| fail 'Hyprland privileged install did not preserve the fully staged reviewed key'
|
|
assert_file_bytes "$test_tmp/cases/hyprland/etc/yum.repos.d/panama-hyprland.repo" "$(cat <<'EXPECTED'
|
|
[panama-hyprland]
|
|
name=Panama reviewed Hyprland COPR
|
|
baseurl=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/
|
|
enabled=1
|
|
gpgcheck=1
|
|
repo_gpgcheck=0
|
|
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland
|
|
EXPECTED
|
|
)"
|
|
assert_root_snapshot_logged hyprland hyprland-copr.asc
|
|
assert_root_snapshot_logged hyprland panama-hyprland.repo
|
|
|
|
# The expected root-snapshot digests must already be bound to the fingerprinted
|
|
# key and the script-authored repository stream. A swap before root staging may
|
|
# not become the new expected digest.
|
|
for pre_snapshot_spec in \
|
|
'authored-repo STUB_SWAP_AFTER_REPO_WRITE panama-hyprland.repo'; do
|
|
read -r suffix swap_name swap_target <<<"$pre_snapshot_spec"
|
|
reset_installer_fixture
|
|
name="hyprland-pre-snapshot-$suffix"
|
|
printf -v "$swap_name" '%s' "$swap_target"
|
|
export "$swap_name"
|
|
expect_failure run_installer_function "$name" configure_hyprland_repository
|
|
unset "$swap_name"
|
|
assert_user_source_swapped "$name"
|
|
[[ ! -e "$test_tmp/cases/$name/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland" \
|
|
&& ! -e "$test_tmp/cases/$name/etc/yum.repos.d/panama-hyprland.repo" ]] \
|
|
|| fail "$name published bytes swapped before privileged staging"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
# A same-UID attacker can present malicious bytes to both digest reads while
|
|
# presenting reviewed bytes only to GPG. Verification must therefore inspect
|
|
# an immutable root-owned snapshot, not a toggled user pathname.
|
|
reset_installer_fixture
|
|
STUB_TOGGLE_KEY_VERIFY=hyprland-copr.asc \
|
|
expect_failure run_installer_function hyprland-key-toggle configure_hyprland_repository
|
|
assert_user_source_swapped hyprland-key-toggle
|
|
[[ ! -e "$test_tmp/cases/hyprland-key-toggle/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland" \
|
|
&& ! -e "$test_tmp/cases/hyprland-key-toggle/etc/yum.repos.d/panama-hyprland.repo" ]] \
|
|
|| fail 'same-UID key toggle published attacker-controlled bytes'
|
|
assert_no_runtime_staging hyprland-key-toggle
|
|
|
|
for pair_snapshot in hyprland-copr.asc panama-hyprland.repo; do
|
|
reset_installer_fixture
|
|
name="hyprland-root-bound-${pair_snapshot%.*}"
|
|
STUB_SWAP_AFTER_ROOT_STAGE="$pair_snapshot" \
|
|
expect_success run_installer_function "$name" configure_hyprland_repository
|
|
assert_user_source_swapped "$name"
|
|
cmp -s "$test_tmp/cases/hyprland/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland" \
|
|
"$test_tmp/cases/$name/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland" \
|
|
|| fail "$name privileged key bytes differed after the source swap"
|
|
cmp -s "$test_tmp/cases/hyprland/etc/yum.repos.d/panama-hyprland.repo" \
|
|
"$test_tmp/cases/$name/etc/yum.repos.d/panama-hyprland.repo" \
|
|
|| fail "$name privileged repository bytes differed after the source swap"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function flathub ensure_flathub_remote
|
|
assert_log flathub "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
curl:https://flathub.org/repo/flathub.flatpakrepo:max=1048576:output=flathub.flatpakrepo
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:flathub-key.asc
|
|
sudo:root-verify:flathub-key.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:6E5C05D979C76DAF93C081354184DD4D907A7CAE
|
|
sudo:root-private
|
|
sudo:flatpak remote-add --if-not-exists --gpg-import=FLATHUB_KEY flathub https://dl.flathub.org/repo/
|
|
gpg:fingerprint:6E5C05D979C76DAF93C081354184DD4D907A7CAE
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
assert_root_snapshot_logged flathub flathub-key.asc
|
|
|
|
reset_installer_fixture
|
|
STUB_SWAP_AFTER_ROOT_STAGE=flathub-key.asc \
|
|
expect_success run_installer_function flathub-root-bound ensure_flathub_remote
|
|
assert_user_source_swapped flathub-root-bound
|
|
assert_no_runtime_staging flathub-root-bound
|
|
|
|
reset_installer_fixture
|
|
STUB_TOGGLE_KEY_VERIFY=flathub-key.asc \
|
|
expect_failure run_installer_function flathub-key-toggle ensure_flathub_remote
|
|
assert_user_source_swapped flathub-key-toggle
|
|
assert_file_bytes "$test_tmp/cases/flathub-key-toggle/flatpak-state" 'preserved'
|
|
[[ "$(<"$test_tmp/cases/flathub-key-toggle/commands.log")" != *'sudo:flatpak'* ]] \
|
|
|| fail 'same-UID Flathub key toggle reached remote activation'
|
|
assert_no_runtime_staging flathub-key-toggle
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function claude-code install_claude_code
|
|
assert_log claude-code "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:claude-code.asc
|
|
sudo:root-verify:claude-code.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
|
|
sudo:root-private
|
|
sudo:root-verify:claude-code.asc
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:claude-code.repo
|
|
sudo:root-verify:claude-code.repo
|
|
sudo:install:claude-code.asc:/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama
|
|
sudo:install:claude-code.repo:/etc/yum.repos.d/claude-code.repo
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:panama-bound-claude-code.asc
|
|
sudo:root-verify:panama-bound-claude-code.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
|
|
sudo:root-private
|
|
sudo:dnf install -y --repofrompath panama-bound-claude-code,https://downloads.claude.ai/claude-code/rpm/stable --repo=panama-bound-claude-code --repo=fedora --repo=updates --from-repo=panama-bound-claude-code --setopt=panama-bound-claude-code.gpgcheck=1 --setopt=panama-bound-claude-code.repo_gpgcheck=1 --setopt=panama-bound-claude-code.gpgkey=file://BOUND_KEY claude-code
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
cmp -s "$installer_fixture/setup/provenance/keys/claude-code.asc" \
|
|
"$test_tmp/cases/claude-code/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama" \
|
|
|| fail 'Claude Code privileged install did not preserve the fully staged reviewed key'
|
|
assert_file_bytes "$test_tmp/cases/claude-code/etc/yum.repos.d/claude-code.repo" "$(cat <<'EXPECTED'
|
|
[claude-code]
|
|
name=Claude Code
|
|
baseurl=https://downloads.claude.ai/claude-code/rpm/stable
|
|
enabled=1
|
|
gpgcheck=1
|
|
repo_gpgcheck=1
|
|
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama
|
|
EXPECTED
|
|
)"
|
|
assert_root_snapshot_logged claude-code claude-code.asc
|
|
assert_root_snapshot_logged claude-code claude-code.repo
|
|
grep -q -- '--repofrompath panama-bound-claude-code,' \
|
|
"$test_tmp/cases/claude-code/commands.log" \
|
|
|| fail 'Claude Code install did not bind the reviewed repository URL'
|
|
|
|
for pair_snapshot in claude-code.asc claude-code.repo; do
|
|
reset_installer_fixture
|
|
name="claude-code-root-bound-${pair_snapshot%.*}"
|
|
STUB_SWAP_AFTER_ROOT_STAGE="$pair_snapshot" \
|
|
expect_success run_installer_function "$name" install_claude_code
|
|
assert_user_source_swapped "$name"
|
|
cmp -s "$test_tmp/cases/claude-code/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama" \
|
|
"$test_tmp/cases/$name/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama" \
|
|
|| fail "$name privileged key bytes differed after the source swap"
|
|
cmp -s "$test_tmp/cases/claude-code/etc/yum.repos.d/claude-code.repo" \
|
|
"$test_tmp/cases/$name/etc/yum.repos.d/claude-code.repo" \
|
|
|| fail "$name privileged repository bytes differed after the source swap"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function claude-desktop-absent install_claude_desktop_if_trusted
|
|
assert_log claude-desktop-absent 'rpm:release'
|
|
[[ "$(grep -c 'Claude Desktop is optional; configure its reviewed local-key repository manually' \
|
|
"$test_tmp/cases/claude-desktop-absent/output")" -eq 1 ]] \
|
|
|| fail 'absent Claude Desktop repository did not produce exactly one manual message'
|
|
|
|
# Manual configuration is only an operator-consent gate. User-owned keys,
|
|
# symlinked repo files, and alternate metadata sources must never become the
|
|
# privileged DNF trust source even when their visible values look reviewed.
|
|
for mode in home-key symlink-repo metalink mirrorlist; do
|
|
reset_installer_fixture
|
|
name="claude-desktop-$mode"
|
|
STUB_CLAUDE_DESKTOP_REPO_MODE="$mode" \
|
|
expect_success run_installer_function "$name" install_claude_desktop_if_trusted
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:dnf'* ]] \
|
|
|| fail "Claude Desktop $mode configuration reached package activation"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
STUB_CLAUDE_DESKTOP_REPO_MODE=trusted \
|
|
expect_success run_installer_function claude-desktop-trusted install_claude_desktop_if_trusted
|
|
assert_log claude-desktop-trusted "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
gpg:fingerprint:825A7D15D78BABE45646D5DF382409F597908867
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:claude-desktop.asc
|
|
sudo:root-verify:claude-desktop.asc
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:825A7D15D78BABE45646D5DF382409F597908867
|
|
sudo:root-private
|
|
sudo:dnf install -y --repofrompath panama-claude-desktop,https://patrickjaja.github.io/claude-desktop-extra/rpm/ --repo=panama-claude-desktop --repo=fedora --repo=updates --from-repo=panama-claude-desktop --setopt=panama-claude-desktop.gpgcheck=1 --setopt=panama-claude-desktop.repo_gpgcheck=1 --setopt=panama-claude-desktop.gpgkey=file://CLAUDE_DESKTOP_KEY claude-desktop-extra
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
assert_root_snapshot_logged claude-desktop-trusted claude-desktop.asc
|
|
assert_no_runtime_staging claude-desktop-trusted
|
|
|
|
# Existing repository state is part of the trust boundary. Idempotency is only
|
|
# success when the already-active repository matches the reviewed policy.
|
|
reset_installer_fixture
|
|
STUB_FLATPAK_REMOTE_MODE=trusted \
|
|
expect_success run_installer_function flathub-existing-trusted ensure_flathub_remote
|
|
assert_log flathub-existing-trusted "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
gpg:fingerprint:6E5C05D979C76DAF93C081354184DD4D907A7CAE
|
|
EXPECTED
|
|
)"
|
|
assert_file_bytes "$test_tmp/cases/flathub-existing-trusted/flatpak-state" 'preserved'
|
|
|
|
for mode in wrong-url wrong-key no-gpg alternate-key empty-alternate-key \
|
|
duplicate-alternate-key malformed-alternate-key trusted-config-symlink \
|
|
trusted-key-symlink; do
|
|
reset_installer_fixture
|
|
name="flathub-existing-$mode"
|
|
STUB_FLATPAK_REMOTE_MODE="$mode" \
|
|
expect_failure run_installer_function "$name" ensure_flathub_remote
|
|
assert_file_bytes "$test_tmp/cases/$name/flatpak-state" 'preserved'
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'curl:'* \
|
|
&& "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:'* ]] \
|
|
|| fail "untrusted existing Flathub $mode state was changed"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=trusted \
|
|
expect_success run_installer_function terra-existing-trusted install_terra_repository
|
|
assert_log terra-existing-trusted "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
dnf:dump-all:locale=C
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
EXPECTED
|
|
)"
|
|
|
|
for mode in nogpg wrong-url wrong-key trusted-key-symlink trusted-repo-symlink; do
|
|
reset_installer_fixture
|
|
name="terra-existing-$mode"
|
|
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE="$mode" \
|
|
expect_failure run_installer_function "$name" install_terra_repository
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:'* ]] \
|
|
|| fail "untrusted existing Terra $mode state reached a mutation"
|
|
done
|
|
|
|
# A historical terra-release package without an active repo is recoverable:
|
|
# direct reviewed-pair publication does not depend on or mutate package state.
|
|
reset_installer_fixture
|
|
STUB_TERRA_INSTALLED=1 \
|
|
expect_success run_installer_function terra-existing-package-only \
|
|
install_terra_repository
|
|
[[ "$(<"$test_tmp/cases/terra-existing-package-only/commands.log")" != *'sudo:dnf'* ]] \
|
|
|| fail 'historical terra-release state triggered a bootstrap DNF transaction'
|
|
|
|
# An optional security field may be absent, but duplicates are malformed even
|
|
# when one copy looks safe. These cases catch the absent/duplicate conflation.
|
|
for duplicate_case in \
|
|
$'duplicate-no-gpg NoGPGVerify=false\nNoGPGVerify=true' \
|
|
$'duplicate-gpg-verify GPGVerify=true\nGPGVerify=false' \
|
|
'alternate-gpg-key-path GPGKeyPath=/unreviewed/keyring.gpg' \
|
|
'empty-gpg-key-path GPGKeyPath=' \
|
|
$'duplicate-gpg-key-path GPGKeyPath=\nGPGKeyPath=/unreviewed/keyring.gpg' \
|
|
'malformed-gpg-key-path GPGKeyPath /unreviewed/keyring.gpg'; do
|
|
name="${duplicate_case%% *}"
|
|
lines="${duplicate_case#* }"
|
|
reset_installer_fixture
|
|
STUB_FLATHUB_VERIFY_LINE="$lines" \
|
|
expect_failure run_installer_function "$name" ensure_flathub_remote
|
|
assert_file_bytes "$test_tmp/cases/$name/flatpak-state" 'preserved'
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:'* ]] \
|
|
|| fail "$name descriptor reached remote activation"
|
|
done
|
|
|
|
# DNF's non-networking effective configuration dump, rather than any one repo
|
|
# file, decides whether Terra is absent, trusted, overridden, or duplicated.
|
|
reset_installer_fixture
|
|
STUB_TERRA_EFFECTIVE_MODE=absent \
|
|
expect_success run_installer_function terra-effective-absent preflight_terra_trust
|
|
assert_log terra-effective-absent 'dnf:dump-all:locale=C'
|
|
|
|
reset_installer_fixture
|
|
STUB_TERRA_EFFECTIVE_MODE=trusted STUB_TERRA_REPO_MODE=trusted \
|
|
expect_success run_installer_function terra-effective-trusted preflight_terra_trust
|
|
assert_log terra-effective-trusted "$(cat <<'EXPECTED'
|
|
dnf:dump-all:locale=C
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
EXPECTED
|
|
)"
|
|
|
|
for mode in legacy override-url override-gpg duplicate alternate; do
|
|
reset_installer_fixture
|
|
name="terra-effective-$mode"
|
|
status=0
|
|
STUB_TERRA_EFFECTIVE_MODE="$mode" STUB_TERRA_REPO_MODE=trusted \
|
|
run_installer_function "$name" preflight_terra_trust || status=$?
|
|
[[ "$status" -eq 78 ]] \
|
|
|| fail "effective Terra $mode returned $status instead of hard trust status 78"
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:'* ]] \
|
|
|| fail "effective Terra $mode reached a transaction"
|
|
done
|
|
|
|
# The full effective configuration must reveal case variants and mixed-case
|
|
# alternate identities that a lowercase terra* selector omits.
|
|
for identity_case in uppercase mixed-case mixed-alternate; do
|
|
reset_installer_fixture
|
|
name="terra-effective-$identity_case"
|
|
status=0
|
|
STUB_TERRA_EFFECTIVE_MODE="$identity_case" STUB_TERRA_REPO_MODE=trusted \
|
|
run_installer_function "$name" preflight_terra_trust || status=$?
|
|
[[ "$status" -eq 78 ]] \
|
|
|| fail "effective Terra $identity_case returned $status instead of hard trust status 78"
|
|
done
|
|
|
|
# DNF output must be locale-stable, and nonempty output that does not match the
|
|
# machine format is unsafe rather than equivalent to a fresh host.
|
|
for locale_case in locale-unsafe localized-output; do
|
|
reset_installer_fixture
|
|
name="terra-effective-$locale_case"
|
|
status=0
|
|
STUB_CALLER_LOCALE=C.UTF-8 STUB_TERRA_EFFECTIVE_MODE="$locale_case" \
|
|
run_installer_function "$name" preflight_terra_trust || status=$?
|
|
[[ "$status" -eq 78 ]] \
|
|
|| fail "effective Terra $locale_case returned $status instead of hard trust status 78"
|
|
assert_log "$name" 'dnf:dump-all:locale=C'
|
|
done
|
|
|
|
assert_pair_rollback() {
|
|
local name="$1" pair="$2" prior="$3" key repo
|
|
case "$pair" in
|
|
hyprland)
|
|
key="$test_tmp/cases/$name/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland"
|
|
repo="$test_tmp/cases/$name/etc/yum.repos.d/panama-hyprland.repo"
|
|
;;
|
|
claude-code)
|
|
key="$test_tmp/cases/$name/etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama"
|
|
repo="$test_tmp/cases/$name/etc/yum.repos.d/claude-code.repo"
|
|
;;
|
|
esac
|
|
if [[ "$prior" == present ]]; then
|
|
assert_file_bytes "$key" 'known key'
|
|
assert_file_bytes "$repo" 'known repo'
|
|
else
|
|
[[ ! -e "$key" && ! -e "$repo" ]] \
|
|
|| fail "$pair activation failure left part of an absent pair"
|
|
fi
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:dnf'* ]] \
|
|
|| fail "$pair activation failure reached DNF"
|
|
}
|
|
|
|
# Both activation writes can fail after changing their target. Each repository
|
|
# must restore known-good pairs and return prior-absent pairs to full absence.
|
|
for pair_spec in \
|
|
'hyprland configure_hyprland_repository' \
|
|
'claude-code install_claude_code'; do
|
|
read -r pair function_name <<<"$pair_spec"
|
|
for prior in absent present; do
|
|
for fail_at in 1 2; do
|
|
reset_installer_fixture
|
|
name="$pair-$prior-activation-$fail_at"
|
|
STUB_PAIR_NAME="$pair" STUB_PAIR_PRIOR="$prior" STUB_INSTALL_FAIL_AT="$fail_at" \
|
|
expect_failure run_installer_function "$name" "$function_name"
|
|
assert_pair_rollback "$name" "$pair" "$prior"
|
|
done
|
|
done
|
|
done
|
|
|
|
# Rollback must be uninterruptible once a signal starts it. Deliver a second
|
|
# TERM while the first prior file is being restored and require the complete
|
|
# prior pair, not a half-restored trust root.
|
|
reset_installer_fixture
|
|
STUB_PAIR_NAME=claude-code STUB_PAIR_PRIOR=present \
|
|
STUB_SIGNAL_PAIR_AFTER_FIRST=1 STUB_SIGNAL_PAIR_TWICE=1 \
|
|
expect_failure run_installer_function claude-code-present-double-signal \
|
|
install_claude_code 2>/dev/null
|
|
grep -qFx 'signal:repository-pair-second' \
|
|
"$test_tmp/cases/claude-code-present-double-signal/commands.log" \
|
|
|| fail 'double-signal case did not deliver the rollback signal'
|
|
assert_pair_rollback claude-code-present-double-signal claude-code present
|
|
assert_no_runtime_staging claude-code-present-double-signal
|
|
|
|
# Recovery evidence is intentionally retained when rollback itself fails, and
|
|
# that trust-root failure must cross the public optional-install wrapper as 78.
|
|
# Converting it to an ordinary soft failure would let the package stage keep
|
|
# running DNF transactions after a repository pair was left indeterminate.
|
|
reset_installer_fixture
|
|
rollback_failure_status=0
|
|
STUB_PAIR_NAME=claude-code STUB_PAIR_PRIOR=absent STUB_INSTALL_FAIL_AT=2 \
|
|
STUB_ROLLBACK_FAIL=1 \
|
|
run_installer_function claude-code-rollback-failure install_claude_code \
|
|
|| rollback_failure_status=$?
|
|
[[ "$rollback_failure_status" -eq 78 ]] \
|
|
|| fail "Claude Code rollback failure returned $rollback_failure_status instead of 78"
|
|
[[ -z "$(find "$test_tmp/cases/claude-code-rollback-failure/root-staging" \
|
|
-mindepth 1 -print -quit)" ]] \
|
|
&& fail 'Claude Code rollback failure discarded its recovery evidence'
|
|
[[ ! -s "$test_tmp/cases/claude-code-rollback-failure/softly-failed" ]] \
|
|
|| fail 'Claude Code rollback failure was downgraded to a soft failure'
|
|
|
|
agent_boundary_status=0
|
|
run_installer_function agent-trust-boundary exercise_agent_install_boundary \
|
|
|| agent_boundary_status=$?
|
|
[[ "$agent_boundary_status" -eq 78 ]] \
|
|
|| fail "agent install trust boundary returned $agent_boundary_status instead of 78"
|
|
assert_log agent-trust-boundary "$(cat <<'EXPECTED'
|
|
agent:node
|
|
agent:pnpm
|
|
agent:bun
|
|
agent:claude:78
|
|
EXPECTED
|
|
)"
|
|
|
|
# A real signal between the two activation writes follows the same rollback
|
|
# path as an ordinary failure, for both prior-present and prior-absent pairs.
|
|
for pair_spec in \
|
|
'hyprland configure_hyprland_repository' \
|
|
'claude-code install_claude_code'; do
|
|
read -r pair function_name <<<"$pair_spec"
|
|
for prior in absent present; do
|
|
reset_installer_fixture
|
|
name="$pair-$prior-signal-after-first"
|
|
STUB_PAIR_NAME="$pair" STUB_PAIR_PRIOR="$prior" \
|
|
STUB_SIGNAL_PAIR_AFTER_FIRST=1 \
|
|
expect_failure run_installer_function "$name" "$function_name" 2>/dev/null
|
|
grep -qFx 'signal:repository-pair' "$test_tmp/cases/$name/commands.log" \
|
|
|| fail "$name did not deliver its real process-group signal"
|
|
assert_pair_rollback "$name" "$pair" "$prior"
|
|
assert_no_runtime_staging "$name"
|
|
done
|
|
done
|
|
|
|
# A Fedora version outside the reviewed policy stops every public transaction
|
|
# before curl, sudo, Flatpak, or repository inspection can act.
|
|
for function_name in install_rpmfusion_repositories install_terra_repository \
|
|
configure_hyprland_repository ensure_flathub_remote install_pnpm install_claude_code \
|
|
install_claude_desktop_if_trusted; do
|
|
reset_installer_fixture
|
|
name="wrong-fedora-${function_name}"
|
|
STUB_FEDORA_RELEASE=45 expect_failure run_installer_function "$name" "$function_name"
|
|
assert_log "$name" 'rpm:release'
|
|
if [[ "$function_name" == install_claude_code ]]; then
|
|
assert_soft_failure "$name" 'Claude Code'
|
|
elif [[ "$function_name" == install_pnpm ]]; then
|
|
assert_soft_failure "$name" pnpm
|
|
fi
|
|
done
|
|
|
|
reset_installer_fixture
|
|
sed -i 's#^RPMFUSION_FREE_RELEASE_URL=.*#RPMFUSION_FREE_RELEASE_URL=https://evil.invalid/free.rpm#' \
|
|
"$installer_fixture/setup/provenance/installers.conf"
|
|
expect_failure run_installer_function rpmfusion-wrong-url install_rpmfusion_repositories
|
|
assert_log rpmfusion-wrong-url 'rpm:release'
|
|
|
|
for policy_case in \
|
|
'terra-wrong-url TERRA_BASEURL install_terra_repository' \
|
|
'hyprland-wrong-url HYPRLAND_COPR_BASEURL configure_hyprland_repository' \
|
|
'flathub-wrong-url FLATHUB_DESCRIPTOR_URL ensure_flathub_remote' \
|
|
'claude-code-wrong-url CLAUDE_CODE_BASEURL install_claude_code' \
|
|
'claude-desktop-wrong-url CLAUDE_DESKTOP_BASEURL install_claude_desktop_if_trusted'; do
|
|
read -r name config_name function_name <<<"$policy_case"
|
|
reset_installer_fixture
|
|
sed -i "s#^${config_name}=.*#${config_name}=https://evil.invalid/#" \
|
|
"$installer_fixture/setup/provenance/installers.conf"
|
|
expect_failure run_installer_function "$name" "$function_name"
|
|
[[ "$(<"$test_tmp/cases/$name/commands.log")" != *'curl:'* \
|
|
&& "$(<"$test_tmp/cases/$name/commands.log")" != *'sudo:'* ]] \
|
|
|| fail "$config_name mismatch reached a download or mutation"
|
|
done
|
|
|
|
reset_installer_fixture
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$installer_fixture/setup/provenance/keys/rpmfusion-free.asc"
|
|
expect_failure run_installer_function rpmfusion-wrong-key install_rpmfusion_repositories
|
|
assert_log rpmfusion-wrong-key "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
curl:https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm:max=4194304:output=rpmfusion-free-release.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rpmfusion-free-release.rpm
|
|
sudo:root-verify:rpmfusion-free-release.rpm
|
|
sudo:root-create
|
|
sudo:root-private
|
|
sudo:root-stage:rpmfusion-free.asc
|
|
sudo:root-verify:rpmfusion-free.asc
|
|
sudo:root-reviewable
|
|
sudo:root-reviewable
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
sudo:root-private
|
|
sudo:root-private
|
|
sudo:root-cleanup
|
|
sudo:root-cleanup
|
|
EXPECTED
|
|
)"
|
|
|
|
reset_installer_fixture
|
|
STUB_RPM_SIGNATURE_FAIL=rpmfusion-free-release.rpm \
|
|
expect_failure run_installer_function rpmfusion-bad-signature install_rpmfusion_repositories
|
|
[[ "$(<"$test_tmp/cases/rpmfusion-bad-signature/commands.log")" != *'rpmfusion-nonfree'* ]] \
|
|
|| fail 'RPM Fusion signature failure did not stop the dependent download'
|
|
[[ "$(<"$test_tmp/cases/rpmfusion-bad-signature/commands.log")" != *'sudo:dnf'* ]] \
|
|
|| fail 'RPM Fusion signature failure reached package activation'
|
|
assert_no_runtime_staging rpmfusion-bad-signature
|
|
|
|
reset_installer_fixture
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$installer_fixture/setup/provenance/keys/hyprland-copr.asc"
|
|
STUB_EXISTING_REPOSITORY=hyprland \
|
|
expect_failure run_installer_function hyprland-wrong-key configure_hyprland_repository
|
|
assert_file_bytes "$test_tmp/cases/hyprland-wrong-key/etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland" \
|
|
'known key'
|
|
assert_file_bytes "$test_tmp/cases/hyprland-wrong-key/etc/yum.repos.d/panama-hyprland.repo" \
|
|
'known repo'
|
|
[[ "$(<"$test_tmp/cases/hyprland-wrong-key/commands.log")" != *'sudo:install:'* ]] \
|
|
|| fail 'Hyprland key mismatch replaced known-good repository files'
|
|
assert_no_runtime_staging hyprland-wrong-key
|
|
|
|
reset_installer_fixture
|
|
STUB_FLATHUB_VERIFY_LINE='NoGPGVerify=true' \
|
|
expect_failure run_installer_function flathub-no-gpg ensure_flathub_remote
|
|
assert_file_bytes "$test_tmp/cases/flathub-no-gpg/flatpak-state" 'preserved'
|
|
[[ "$(<"$test_tmp/cases/flathub-no-gpg/commands.log")" != *'sudo:'* ]] \
|
|
|| fail 'Flathub disabled-GPG descriptor mutated a remote'
|
|
|
|
reset_installer_fixture
|
|
STUB_FLATHUB_KEY_FILE="$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
expect_failure run_installer_function flathub-wrong-key ensure_flathub_remote
|
|
assert_file_bytes "$test_tmp/cases/flathub-wrong-key/flatpak-state" 'preserved'
|
|
[[ "$(<"$test_tmp/cases/flathub-wrong-key/commands.log")" != *'sudo:flatpak'* ]] \
|
|
|| fail 'Flathub key mismatch mutated an existing remote'
|
|
assert_no_runtime_staging flathub-wrong-key
|
|
|
|
reset_installer_fixture
|
|
STUB_FLATHUB_URL='https://evil.invalid/repo/' \
|
|
expect_failure run_installer_function flathub-wrong-repo-url ensure_flathub_remote
|
|
assert_file_bytes "$test_tmp/cases/flathub-wrong-repo-url/flatpak-state" 'preserved'
|
|
[[ "$(<"$test_tmp/cases/flathub-wrong-repo-url/commands.log")" != *'sudo:'* ]] \
|
|
|| fail 'Flathub repository URL mismatch mutated an existing remote'
|
|
|
|
reset_installer_fixture
|
|
STUB_CLAUDE_DESKTOP_REPO_MODE=untrusted \
|
|
expect_success run_installer_function claude-desktop-untrusted install_claude_desktop_if_trusted
|
|
assert_log claude-desktop-untrusted 'rpm:release'
|
|
[[ "$(grep -c 'Claude Desktop is optional; configure its reviewed local-key repository manually' \
|
|
"$test_tmp/cases/claude-desktop-untrusted/output")" -eq 1 ]] \
|
|
|| fail 'untrusted Claude Desktop repository did not produce one manual message'
|
|
|
|
reset_installer_fixture
|
|
terra_failure_status=0
|
|
STUB_INSTALL_FAIL_AT=2 \
|
|
run_installer_function terra-publication-failure install_terra_repository \
|
|
|| terra_failure_status=$?
|
|
[[ "$terra_failure_status" -eq 78 ]] \
|
|
|| fail "Terra publication failure returned $terra_failure_status instead of 78"
|
|
[[ "$(<"$test_tmp/cases/terra-publication-failure/commands.log")" != *'terra-release'* \
|
|
&& ! -s "$test_tmp/cases/terra-publication-failure/terra-rpm-state" ]] \
|
|
|| fail 'Terra direct publication changed terra-release package state'
|
|
[[ ! -e "$test_tmp/cases/terra-publication-failure/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama" \
|
|
&& ! -e "$test_tmp/cases/terra-publication-failure/etc/yum.repos.d/terra.repo" ]] \
|
|
|| fail 'Terra publication failure did not remove its partial repository pair'
|
|
assert_no_runtime_staging terra-publication-failure
|
|
|
|
STUB_REUSE_CASE=1 \
|
|
expect_success run_installer_function terra-publication-failure \
|
|
install_terra_repository
|
|
|
|
[[ "$before_gnupg" == "$(snapshot "$host_gnupg")" ]] || fail 'repository cases changed host GPG state'
|
|
[[ "$before_gpg_files" == "$(snapshot_gpg_state "$host_gnupg")" ]] \
|
|
|| fail 'repository cases changed host GPG files'
|
|
[[ "$before_rpmdb" == "$(snapshot "$host_rpmdb")" ]] || fail 'repository cases changed host RPM database'
|
|
[[ "$before_repo_files" == "$(snapshot_gpg_state /etc/yum.repos.d)" ]] \
|
|
|| fail 'repository cases changed host repository files'
|
|
[[ "$before_rpm_key_files" == "$(snapshot_gpg_state /etc/pki/rpm-gpg)" ]] \
|
|
|| fail 'repository cases changed host RPM key files'
|
|
[[ "$before_system_flatpak" == "$(snapshot_file_state /var/lib/flatpak/repo/config)" ]] \
|
|
|| fail 'repository cases changed the system Flatpak remote'
|
|
[[ "$before_system_flathub_key" == "$(snapshot_file_state /var/lib/flatpak/repo/flathub.trustedkeys.gpg)" ]] \
|
|
|| fail 'repository cases changed the system Flathub trusted key'
|
|
[[ "$before_user_flatpak" == "$(snapshot_file_state "$HOME/.local/share/flatpak/repo/config")" ]] \
|
|
|| fail 'repository cases changed the user Flatpak remote'
|
|
[[ "$before_bashrc" == "$(snapshot_file_state "$HOME/.bashrc")" ]] \
|
|
|| fail 'repository cases changed the protected bashrc'
|
|
|
|
printf 'package provenance contract: PASS\n'
|