The repository audit made any Terra that is not Panama's own pinned form a trust-root failure, and status 78 then stopped every stage before it ran. A machine that installed Terra the way Terra documents it -- terra-release's own repo file, a metalink, the key at its stock path -- was classified hostile and had no way back, because install_terra_repository refused to touch a machine terra-release had already reached. A gate with no door. The trust root is the signing key, and that key is byte-for-byte the fingerprint this repository reviewed and pinned, with every signature check already on. So verify the fingerprint and adopt the configuration into the pinned form instead of refusing it. Adoption needs no network and no DNF, it runs before any other transaction in the stage, and it is repeatable, which it has to be: terra-release owns that file and restores it on update. Adoption stays narrow. The pinned fingerprint must match both the reviewed key and the key the machine actually verifies against, the gpgkey must be a local file under the system trust directory, and the endpoint must be one Terra itself serves -- so the reviewed baseurl or the reviewed metalink host, now pinned as TERRA_METALINK_BASEURL. An unknown key, a redirected baseurl, a second enabled Terra, or a disabled signature check is still a hard refusal. A refusal also stops less than it did. It suppresses the stages that open DNF and the migrations, which may run a transaction of their own, and the run still exits 78. It no longer stops link-dotfiles, link-skills or link-user, which read no repository and install no package. Exiting before them is what left this laptop with a stale ~/.claude/skills and no shipped skill reachable. Also stub ensure_flathub_remote in the extras contract, which has been failing since that call was added to install_extra_category without one. Claude-Session: https://claude.ai/code/session_01PeTrG9dGY89UWuhGm4Pr1s
2069 lines
96 KiB
Bash
Executable File
2069 lines
96 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"
|
|
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
|
|
}
|
|
|
|
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"
|
|
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
|
|
|
|
# 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/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"
|
|
|
|
[[ "$(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 parser must not accept plausible output from a GPG process that failed.
|
|
# The later import and verify calls succeed so both public helpers depend on
|
|
# the show-only producer's status rather than failing for an unrelated reason.
|
|
producer_failure_bin="$test_tmp/gpg-producer-failure-bin"
|
|
mkdir "$producer_failure_bin"
|
|
cat > "$producer_failure_bin/gpg" <<EOF
|
|
#!/usr/bin/env bash
|
|
if [[ " \$* " == *' --import-options show-only '* ]]; then
|
|
printf 'pub:::::::::\n'
|
|
printf 'fpr:::::::::$fixture_fingerprint:\n'
|
|
exit 42
|
|
fi
|
|
exit 0
|
|
EOF
|
|
chmod +x "$producer_failure_bin/gpg"
|
|
expect_failure env PATH="$producer_failure_bin:$PATH" bash -c '
|
|
source "$1"
|
|
key_fingerprint_matches "$2" "$3"
|
|
' _ "$repo_dir/setup/lib/artifact-provenance" "$fixtures/fixture-key.asc" "$fixture_fingerprint"
|
|
expect_failure env PATH="$producer_failure_bin:$PATH" bash -c '
|
|
source "$1"
|
|
verify_detached_signature "$2" "$3" "$4"
|
|
' _ "$repo_dir/setup/lib/artifact-provenance" "$fixtures/fixture-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/chatgpt-package" \
|
|
"$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"
|
|
|
|
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"
|
|
|
|
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/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 ]] ;;
|
|
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_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=''
|
|
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"
|
|
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" == 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:-}" == install ]]; then
|
|
shift
|
|
[[ "${1:-}" == -m && ( "${2:-}" == 0644 || "${2:-}" == 644 ) ]] || exit 67
|
|
source_file="$3"
|
|
destination="$4"
|
|
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 [[ -n "${STUB_INSTALL_FAIL_AT:-}" && "$count" == "$STUB_INSTALL_FAIL_AT" ]]; then
|
|
exit 67
|
|
fi
|
|
exit 0
|
|
fi
|
|
if [[ "${1:-}" == rm && "${2:-}" == -f && "${3:-}" == -- ]]; then
|
|
destination="$4"
|
|
printf 'sudo:rm:%s\n' "$destination" >> "$COMMAND_LOG"
|
|
rm -f -- "$STUB_ETC${destination#/etc}"
|
|
exit 0
|
|
fi
|
|
original="$*"
|
|
logged=()
|
|
for argument in "$@"; do
|
|
if [[ "$argument" == --gpg-import=*/flathub-key.asc ]]; then
|
|
logged+=(--gpg-import=FLATHUB_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" == "$TMPDIR"/tmp.*/rustdesk.rpm \
|
|
&& -s "$VERIFIED_RUSTDESK_INODE"
|
|
&& "$(stat -c '%d:%i' "$rustdesk_path")" == "$(<"$VERIFIED_RUSTDESK_INODE")" ]] \
|
|
&& 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" == *' pnpm' || "$original" == *' claude-code' ]]; then
|
|
case "$original" in
|
|
'dnf install -y --repo=fedora --repo=updates pnpm'|\
|
|
'dnf install -y --repo=claude-code --repo=fedora --repo=updates --from-repo=claude-code claude-code') ;;
|
|
*) exit 71 ;;
|
|
esac
|
|
fi
|
|
if [[ -n "${STUB_DNF_FAIL_MATCH:-}" && "$original" == *"$STUB_DNF_FAIL_MATCH"* ]]; then
|
|
exit 68
|
|
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 ;;
|
|
stock|stock-wrong-key) mode=stock ;;
|
|
esac
|
|
# Adoption rewrites the repository file. Once it is the pinned form the
|
|
# dump has to say so, or the re-verification adoption performs on itself
|
|
# could never pass.
|
|
if [[ "$mode" == stock && -f "$STUB_ETC/yum.repos.d/terra.repo" ]] \
|
|
&& grep -q '^gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama$' \
|
|
"$STUB_ETC/yum.repos.d/terra.repo"; then
|
|
mode=trusted
|
|
fi
|
|
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'
|
|
;;
|
|
stock)
|
|
printf '======== "terra" repository configuration: ========\n'
|
|
printf 'baseurl = \nenabled = 1\ngpgcheck = 1\n'
|
|
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\n'
|
|
printf 'metalink = https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=x86_64\n'
|
|
printf 'mirrorlist = \npkg_gpgcheck = 1\nrepo_gpgcheck = 1\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
|
|
case_root="$test_tmp/cases/$name"
|
|
if [[ "${STUB_REUSE_CASE:-}" != 1 ]]; then
|
|
rm -rf -- "$case_root"
|
|
fi
|
|
make_stub_commands "$case_root"
|
|
: > "$case_root/commands.log"
|
|
printf '0\n' > "$case_root/install-counter"
|
|
: > "$case_root/verified-rustdesk-inode"
|
|
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"
|
|
;;
|
|
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"
|
|
;;
|
|
stock|stock-wrong-key)
|
|
if [[ "${STUB_TERRA_REPO_MODE}" == stock ]]; then
|
|
cp "$installer_fixture/setup/provenance/keys/terra44.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44"
|
|
else
|
|
cp "$installer_fixture/setup/provenance/keys/flathub.asc" \
|
|
"$case_root/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44"
|
|
fi
|
|
printf '[terra]\nmetalink=https://tetsudou.fyralabs.com/metalink?repo=terra44&arch=$basearch\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44\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"
|
|
;;
|
|
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/claude-desktop-local.asc"
|
|
printf '[claude-desktop]\nbaseurl=https://patrickjaja.github.io/claude-desktop-extra/rpm/\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=file://%s\n' \
|
|
"$case_root/etc/pki/rpm-gpg/claude-desktop-local.asc" \
|
|
> "$case_root/etc/yum.repos.d/claude-desktop.repo"
|
|
;;
|
|
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" \
|
|
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" \
|
|
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"; declare -F "$1" >/dev/null; status=0; "$1" || status=$?; (( ${#softly_failed[@]} == 0 )) || printf "%s\n" "${softly_failed[@]}" > "$SOFT_LOG"; exit "$status"' \
|
|
bash "$function_name"
|
|
) > "$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_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)" ]] \
|
|
|| 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
|
|
|
|
# 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:dnf install -y --setopt=localpkg_gpgcheck=0 RUSTDESK_LOCAL
|
|
EXPECTED
|
|
)"
|
|
assert_no_runtime_staging rustdesk-x86_64
|
|
|
|
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 pnpm
|
|
EXPECTED
|
|
)"
|
|
|
|
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
|
|
gpg:fingerprint:E9A491A3DE247814E7E067EAE06F8ECDD651FF2E
|
|
rpmkeys:import:rpmfusion-free.asc
|
|
rpmkeys:checksig:rpmfusion-free-release.rpm
|
|
curl:https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm:max=4194304:output=rpmfusion-nonfree-release.rpm
|
|
gpg:fingerprint:79BDB88F9BBF73910FD4095B6A2AF96194843C65
|
|
rpmkeys:import:rpmfusion-nonfree.asc
|
|
rpmkeys:checksig:rpmfusion-nonfree-release.rpm
|
|
sudo:dnf install -y --setopt=localpkg_gpgcheck=1 RPMFUSION_FREE RPMFUSION_NONFREE
|
|
EXPECTED
|
|
)"
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function terra install_terra_repository
|
|
assert_log terra "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
dnf:dump-all:locale=C
|
|
rpm:query:terra-release
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
sudo:install:terra44.asc:/etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama
|
|
sudo:dnf install -y --repofrompath terra,https://repos.fyralabs.com/terra44 --setopt=terra.pkg_gpgcheck=1 --setopt=terra.repo_gpgcheck=1 --setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama terra-release
|
|
sudo:install:terra.repo:/etc/yum.repos.d/terra.repo
|
|
dnf:dump-all:locale=C
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
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
|
|
)"
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function hyprland configure_hyprland_repository
|
|
assert_log hyprland "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
gpg:fingerprint:97E23476C89635135407C7D5E9BA41342C4B2995
|
|
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
|
|
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
|
|
)"
|
|
|
|
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
|
|
gpg:fingerprint:6E5C05D979C76DAF93C081354184DD4D907A7CAE
|
|
sudo:flatpak remote-add --if-not-exists --gpg-import=FLATHUB_KEY flathub https://dl.flathub.org/repo/
|
|
gpg:fingerprint:6E5C05D979C76DAF93C081354184DD4D907A7CAE
|
|
EXPECTED
|
|
)"
|
|
|
|
reset_installer_fixture
|
|
expect_success run_installer_function claude-code install_claude_code
|
|
assert_log claude-code "$(cat <<'EXPECTED'
|
|
rpm:release
|
|
gpg:fingerprint:31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
|
|
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:dnf install -y --repo=claude-code --repo=fedora --repo=updates --from-repo=claude-code claude-code
|
|
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
|
|
)"
|
|
|
|
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'
|
|
|
|
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
|
|
gpg:fingerprint:825A7D15D78BABE45646D5DF382409F597908867
|
|
sudo:dnf install -y claude-desktop-extra
|
|
EXPECTED
|
|
)"
|
|
|
|
# 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; 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 absent; 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 machine that installed Terra the way Terra documents it. The repository file
|
|
# is terra-release's own -- a metalink, and the key at its stock path -- so it is
|
|
# not Panama's pinned form, but it IS the fingerprint this repository reviewed,
|
|
# with every signature check on. That is an adoption, not a compromise.
|
|
#
|
|
# Refusing it was a gate with no door: the ordinary Fedora desktop could never
|
|
# reach the pinned state, and status 78 then stopped every stage of every run,
|
|
# including the ones that never open DNF.
|
|
reset_installer_fixture
|
|
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock \
|
|
expect_success run_installer_function terra-stock-preflight preflight_terra_trust
|
|
|
|
reset_installer_fixture
|
|
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock \
|
|
expect_success run_installer_function terra-stock-adopt install_terra_repository
|
|
terra_adopted="$test_tmp/cases/terra-stock-adopt/etc/yum.repos.d/terra.repo"
|
|
grep -qx 'baseurl=https://repos.fyralabs.com/terra44' "$terra_adopted" \
|
|
|| fail 'adoption left Terra off the reviewed baseurl'
|
|
grep -qx 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama' "$terra_adopted" \
|
|
|| fail 'adoption left Terra off the reviewed key path'
|
|
grep -q 'metalink' "$terra_adopted" \
|
|
&& fail 'adoption kept the metalink it was supposed to replace'
|
|
[[ "$(<"$test_tmp/cases/terra-stock-adopt/commands.log")" != *'dnf:install'* ]] \
|
|
|| fail 'adoption opened a DNF transaction it does not need'
|
|
|
|
# Adoption is anchored on the fingerprint, not the URL. The same stock shape
|
|
# verifying against a key that is not Terra's is still a hard refusal.
|
|
reset_installer_fixture
|
|
STUB_TERRA_INSTALLED=1 STUB_TERRA_REPO_MODE=stock-wrong-key \
|
|
expect_failure run_installer_function terra-stock-wrong-key install_terra_repository
|
|
[[ "$(<"$test_tmp/cases/terra-stock-wrong-key/commands.log")" != *'sudo:'* ]] \
|
|
|| fail 'a stock Terra signed by an unreviewed key reached a mutation'
|
|
|
|
# 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
|
|
|
|
# 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
|
|
gpg:fingerprint:AE09157A4DE88B497EA1D5D300CDAB43DE226D6F
|
|
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:'* ]] \
|
|
|| fail 'RPM Fusion signature failure reached a privileged mutation'
|
|
|
|
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:'* ]] \
|
|
|| fail 'Hyprland key mismatch replaced known-good repository files'
|
|
|
|
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:'* ]] \
|
|
|| fail 'Flathub key mismatch mutated an existing remote'
|
|
|
|
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
|
|
STUB_DNF_FAIL_MATCH=terra-release expect_failure run_installer_function terra-dnf-failure install_terra_repository
|
|
[[ "$(tail -n 1 "$test_tmp/cases/terra-dnf-failure/commands.log")" == *'terra-release' ]] \
|
|
|| fail 'Terra DNF failure ran a later transaction command'
|
|
[[ -z "$(find "$test_tmp/cases/terra-dnf-failure/tmp" -mindepth 1 -print -quit)" ]] \
|
|
|| fail 'Terra DNF failure left private staging files behind'
|
|
|
|
[[ "$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'
|