diff --git a/setup/scripts/install-packages b/setup/scripts/install-packages index 3eac3d3..3d4b375 100755 --- a/setup/scripts/install-packages +++ b/setup/scripts/install-packages @@ -67,6 +67,15 @@ softly_failed=() # PANAMA_PATH, and clobbering it here made a clone anywhere else source the # extras catalog from a path that does not exist. PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" +# Kept as a named path so the hermetic contract can redirect reads after +# sourcing this file. Normal installer execution always resets it to /etc. +PANAMA_SYSTEM_ETC=/etc + +# Reviewed installer data and verification primitives. The config parser treats +# every value as inert data and rejects unknown, duplicate, or missing fields. +# shellcheck source=../lib/artifact-provenance +source "$PANAMA_PATH/setup/lib/artifact-provenance" +load_installer_provenance "$PANAMA_PATH/setup/provenance/installers.conf" # Reading the extras catalog, shared with `panama apps` so the two front doors # cannot disagree about what a category contains. @@ -152,17 +161,6 @@ install_bun() { fi } -# Claude Code: Anthropic's CLI. The official installer keeps itself updated -# afterwards, so this runs once and then never needs to again. -install_claude_code() { - if command -v claude >/dev/null 2>&1; then - log "Claude Code already installed at \"$(command -v claude)\"" - else - log "Installing Claude Code via the official installer..." - curl -fsSL https://claude.ai/install.sh | bash > /dev/null 2>&1 || { log "Claude Code install failed; skipping"; softly_failed+=("Claude Code"); } - fi -} - # Codex: OpenAI's CLI. Distributed through npm, which is why this runs after # setup_node -- the nvm-managed Node is the one it should land in. install_codex() { @@ -195,6 +193,297 @@ report_soft_failures() { fi } +# --- Reviewed third-party repositories ------------------------------------- + +_require_policy_value() { + local name="$1" expected="$2" + [[ "${INSTALLER_PROVENANCE[$name]:-}" == "$expected" ]] || { + log "Installer provenance for $name does not match Panama's reviewed policy" + return 1 + } +} + +require_reviewed_fedora_release() { + local current + _require_policy_value FEDORA_RELEASE 44 || return 1 + current="$(rpm -E %fedora)" || return 1 + [[ "$current" == "${INSTALLER_PROVENANCE[FEDORA_RELEASE]}" ]] || { + log "Fedora $current is not reviewed for third-party repositories; expected ${INSTALLER_PROVENANCE[FEDORA_RELEASE]}" + return 1 + } +} + +# RPM repository bootstrap packages and Flatpak descriptors are authenticated +# after download rather than by a SHA-256 pin. Keep their untrusted bytes in a +# private file, enforce the reviewed size limit, and publish the file only after +# curl has completed successfully. +_download_bounded() { + local url="$1" max_bytes="$2" destination="$3" directory filename + directory="$(dirname -- "$destination")" + filename="$(basename -- "$destination")" + ( + local part="" + trap '[[ -z "$part" ]] || rm -f -- "$part"' EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + [[ "$max_bytes" =~ ^[1-9][0-9]*$ && -d "$directory" ]] || exit 1 + umask 077 + part="$(mktemp "$directory/.${filename}.part.XXXXXX")" || exit 1 + curl --fail --location --connect-timeout 10 --max-time 600 \ + --max-filesize "$max_bytes" --output "$part" "$url" || exit 1 + [[ -f "$part" && "$(stat -c %s "$part")" -le "$max_bytes" ]] || exit 1 + mv -f -- "$part" "$destination" + ) +} + +_stage_reviewed_key() { + local source_key="$1" destination="$2" fingerprint_name="$3" expected="$4" + _require_policy_value "$fingerprint_name" "$expected" || return 1 + cp -- "$source_key" "$destination" || return 1 + chmod 0600 "$destination" + key_fingerprint_matches "$destination" "${INSTALLER_PROVENANCE[$fingerprint_name]}" +} + +_ini_value() { + local file="$1" wanted_section="$2" wanted_key="$3" + local -a values=() + mapfile -t values < <(awk -v wanted_section="$wanted_section" -v wanted_key="$wanted_key" ' + function trim(value) { + sub(/^[[:space:]]+/, "", value) + sub(/[[:space:]]+$/, "", value) + return value + } + { + sub(/\r$/, "") + line = trim($0) + if (line == "" || line ~ /^[#;]/) next + if (line ~ /^\[[^]]+\]$/) { + section = substr(line, 2, length(line) - 2) + next + } + equals = index(line, "=") + if (tolower(section) == tolower(wanted_section) && equals > 1) { + key = trim(substr(line, 1, equals - 1)) + if (tolower(key) == tolower(wanted_key)) print trim(substr(line, equals + 1)) + } + } + ' "$file") + [[ ${#values[@]} -eq 1 && -n "${values[0]}" ]] || return 1 + printf '%s\n' "${values[0]}" +} + +install_rpmfusion_repositories() { + local work free_rpm nonfree_rpm + require_reviewed_fedora_release || return 1 + _require_policy_value RPMFUSION_FREE_RELEASE_URL \ + 'https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm' || return 1 + _require_policy_value RPMFUSION_FREE_RELEASE_MAX_BYTES 4194304 || return 1 + _require_policy_value RPMFUSION_NONFREE_RELEASE_URL \ + 'https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm' || return 1 + _require_policy_value RPMFUSION_NONFREE_RELEASE_MAX_BYTES 4194304 || return 1 + _require_policy_value RPMFUSION_FREE_FINGERPRINT E9A491A3DE247814E7E067EAE06F8ECDD651FF2E || return 1 + _require_policy_value RPMFUSION_NONFREE_FINGERPRINT 79BDB88F9BBF73910FD4095B6A2AF96194843C65 || return 1 + + work="$(mktemp -d)" || return 1 + chmod 0700 "$work" + free_rpm="$work/rpmfusion-free-release.rpm" + nonfree_rpm="$work/rpmfusion-nonfree-release.rpm" + if ! _download_bounded "${INSTALLER_PROVENANCE[RPMFUSION_FREE_RELEASE_URL]}" \ + "${INSTALLER_PROVENANCE[RPMFUSION_FREE_RELEASE_MAX_BYTES]}" "$free_rpm" \ + || ! rpm_signature_matches "$free_rpm" \ + "$PANAMA_PATH/setup/provenance/keys/rpmfusion-free.asc" \ + "${INSTALLER_PROVENANCE[RPMFUSION_FREE_FINGERPRINT]}" \ + || ! _download_bounded "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_RELEASE_URL]}" \ + "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_RELEASE_MAX_BYTES]}" "$nonfree_rpm" \ + || ! rpm_signature_matches "$nonfree_rpm" \ + "$PANAMA_PATH/setup/provenance/keys/rpmfusion-nonfree.asc" \ + "${INSTALLER_PROVENANCE[RPMFUSION_NONFREE_FINGERPRINT]}"; then + rm -rf -- "$work" + return 1 + fi + local status=0 + sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$free_rpm" "$nonfree_rpm" || status=$? + rm -rf -- "$work" + return "$status" +} + +install_terra_repository() { + local work staged_key status + require_reviewed_fedora_release || return 1 + if rpm -q terra-release >/dev/null 2>&1; then + log "Terra repository already installed" + return 0 + fi + _require_policy_value TERRA_BASEURL 'https://repos.fyralabs.com/terra44' || return 1 + work="$(mktemp -d)" || return 1 + chmod 0700 "$work" + staged_key="$work/terra44.asc" + if ! _stage_reviewed_key "$PANAMA_PATH/setup/provenance/keys/terra44.asc" "$staged_key" \ + TERRA_FINGERPRINT AE09157A4DE88B497EA1D5D300CDAB43DE226D6F; then + rm -rf -- "$work" + return 1 + fi + sudo install -m 0644 "$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama || { + rm -rf -- "$work" + return 1 + } + status=0 + sudo dnf install -y \ + --repofrompath "terra,${INSTALLER_PROVENANCE[TERRA_BASEURL]}" \ + --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 || status=$? + rm -rf -- "$work" + return "$status" +} + +configure_hyprland_repository() { + local work staged_key staged_repo status + require_reviewed_fedora_release || return 1 + _require_policy_value HYPRLAND_COPR_BASEURL \ + 'https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/' \ + || return 1 + work="$(mktemp -d)" || return 1 + chmod 0700 "$work" + staged_key="$work/hyprland-copr.asc" + staged_repo="$work/panama-hyprland.repo" + if ! _stage_reviewed_key "$PANAMA_PATH/setup/provenance/keys/hyprland-copr.asc" "$staged_key" \ + HYPRLAND_COPR_FINGERPRINT 97E23476C89635135407C7D5E9BA41342C4B2995; then + rm -rf -- "$work" + return 1 + fi + printf '%s\n' \ + '[panama-hyprland]' \ + 'name=Panama reviewed Hyprland COPR' \ + "baseurl=${INSTALLER_PROVENANCE[HYPRLAND_COPR_BASEURL]}" \ + 'enabled=1' \ + 'gpgcheck=1' \ + 'repo_gpgcheck=0' \ + 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland' > "$staged_repo" + chmod 0600 "$staged_repo" + status=0 + sudo install -m 0644 "$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland \ + || status=$? + if (( status == 0 )); then + sudo install -m 0644 "$staged_repo" /etc/yum.repos.d/panama-hyprland.repo \ + || status=$? + fi + rm -rf -- "$work" + return "$status" +} + +ensure_flathub_remote() { + local work descriptor encoded key_file url no_gpg_verify gpg_verify status + require_reviewed_fedora_release || return 1 + _require_policy_value FLATHUB_DESCRIPTOR_URL 'https://flathub.org/repo/flathub.flatpakrepo' || return 1 + _require_policy_value FLATHUB_DESCRIPTOR_MAX_BYTES 1048576 || return 1 + _require_policy_value FLATHUB_FINGERPRINT 6E5C05D979C76DAF93C081354184DD4D907A7CAE || return 1 + work="$(mktemp -d)" || return 1 + chmod 0700 "$work" + descriptor="$work/flathub.flatpakrepo" + key_file="$work/flathub-key.asc" + if ! _download_bounded "${INSTALLER_PROVENANCE[FLATHUB_DESCRIPTOR_URL]}" \ + "${INSTALLER_PROVENANCE[FLATHUB_DESCRIPTOR_MAX_BYTES]}" "$descriptor" \ + || ! url="$(_ini_value "$descriptor" 'Flatpak Repo' Url)" \ + || [[ "$url" != 'https://dl.flathub.org/repo/' ]] \ + || ! encoded="$(_ini_value "$descriptor" 'Flatpak Repo' GPGKey)" \ + || ! printf '%s' "$encoded" | base64 --decode > "$key_file"; then + rm -rf -- "$work" + return 1 + fi + if no_gpg_verify="$(_ini_value "$descriptor" 'Flatpak Repo' NoGPGVerify)"; then + case "${no_gpg_verify,,}" in true|yes|1) rm -rf -- "$work"; return 1 ;; esac + fi + if gpg_verify="$(_ini_value "$descriptor" 'Flatpak Repo' GPGVerify)"; then + case "${gpg_verify,,}" in false|no|0) rm -rf -- "$work"; return 1 ;; esac + fi + if ! key_fingerprint_matches "$key_file" "${INSTALLER_PROVENANCE[FLATHUB_FINGERPRINT]}"; then + rm -rf -- "$work" + return 1 + fi + status=0 + sudo flatpak remote-add --if-not-exists --gpg-import="$key_file" flathub "$url" \ + || status=$? + rm -rf -- "$work" + return "$status" +} + +install_claude_code() { + local work staged_key staged_repo status + if command -v claude >/dev/null 2>&1; then + log "Claude Code already installed at \"$(command -v claude)\"" + return 0 + fi + require_reviewed_fedora_release || return 1 + _require_policy_value CLAUDE_CODE_BASEURL 'https://downloads.claude.ai/claude-code/rpm/stable' || return 1 + work="$(mktemp -d)" || return 1 + chmod 0700 "$work" + staged_key="$work/claude-code.asc" + staged_repo="$work/claude-code.repo" + if ! _stage_reviewed_key "$PANAMA_PATH/setup/provenance/keys/claude-code.asc" "$staged_key" \ + CLAUDE_CODE_FINGERPRINT 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE; then + rm -rf -- "$work" + return 1 + fi + printf '%s\n' \ + '[claude-code]' \ + 'name=Claude Code' \ + "baseurl=${INSTALLER_PROVENANCE[CLAUDE_CODE_BASEURL]}" \ + 'enabled=1' \ + 'gpgcheck=1' \ + 'repo_gpgcheck=1' \ + 'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama' > "$staged_repo" + chmod 0600 "$staged_repo" + status=0 + sudo install -m 0644 "$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama \ + || status=$? + if (( status == 0 )); then + sudo install -m 0644 "$staged_repo" /etc/yum.repos.d/claude-code.repo \ + || status=$? + fi + if (( status == 0 )); then + sudo dnf install -y claude-code || status=$? + fi + rm -rf -- "$work" + return "$status" +} + +_claude_desktop_manual() { + log "Claude Desktop is optional; configure its reviewed local-key repository manually to install it" +} + +install_claude_desktop_if_trusted() { + local repo_file baseurl gpgcheck repo_gpgcheck gpgkey local_key + require_reviewed_fedora_release || return 1 + _require_policy_value CLAUDE_DESKTOP_BASEURL \ + 'https://patrickjaja.github.io/claude-desktop-extra/rpm/' || return 1 + _require_policy_value CLAUDE_DESKTOP_FINGERPRINT 825A7D15D78BABE45646D5DF382409F597908867 || return 1 + repo_file="$PANAMA_SYSTEM_ETC/yum.repos.d/claude-desktop.repo" + if [[ ! -f "$repo_file" ]] \ + || ! baseurl="$(_ini_value "$repo_file" claude-desktop baseurl)" \ + || [[ "$baseurl" != "${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_BASEURL]}" ]] \ + || ! gpgcheck="$(_ini_value "$repo_file" claude-desktop gpgcheck)" \ + || [[ "$gpgcheck" != 1 ]] \ + || ! repo_gpgcheck="$(_ini_value "$repo_file" claude-desktop repo_gpgcheck)" \ + || [[ "$repo_gpgcheck" != 1 ]] \ + || ! gpgkey="$(_ini_value "$repo_file" claude-desktop gpgkey)" \ + || [[ "$gpgkey" != file:///* ]]; then + _claude_desktop_manual + return 0 + fi + local_key="${gpgkey#file://}" + if [[ ! -f "$local_key" ]] \ + || ! key_fingerprint_matches "$PANAMA_PATH/setup/provenance/keys/claude-desktop.asc" \ + "${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_FINGERPRINT]}" \ + || ! key_fingerprint_matches "$local_key" \ + "${INSTALLER_PROVENANCE[CLAUDE_DESKTOP_FINGERPRINT]}"; then + _claude_desktop_manual + return 0 + fi + sudo dnf install -y claude-desktop-extra +} + # --- The server path --------------------------------------------------------- # # Everything a server runs is above this line plus the lists it installs. No @@ -217,7 +506,7 @@ fi echo -e "\n--- Installing Repositories ---" log "Installing RPM Fusion Free and Nonfree Repositories" -sudo dnf install -y https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm > /dev/null +install_rpmfusion_repositories > /dev/null log "Enabling Fedora Cisco OpenH264 Repository" # soft: this repo does not exist on every spin, and its absence must not cost # the desktop -- the ordering rule at soft()'s definition applies to the @@ -236,12 +525,8 @@ soft "the RPM Fusion appstream metadata" sudo dnf install -y rpmfusion-\*-appstr # in the repository section, above everything, so `set -e` ended the stage # before a single package was considered. An installer whose second run does # less than its first is worse than one that never ran. -if rpm -q terra-release >/dev/null 2>&1; then - log "Terra repository already installed" -else - log "Installing Terra Repository" - sudo dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release > /dev/null -fi +log "Installing Terra Repository" +install_terra_repository > /dev/null echo -e "\n--- Installing relevant packages ---" log "Updating all packages. This may take a while" @@ -258,16 +543,16 @@ install_list desktop-packages "Desktop" # --- Install the Hyprland desktop --- # -# Directly after desktop-packages, which is what supplies the dnf plugin that -# `dnf copr` needs, and deliberately before anything optional. This is the one +# Directly after desktop-packages and deliberately before anything optional. +# The reviewed local repository below supplies these packages. This is the one # thing on the list that Panama is; a machine that gets only this far is a # machine you can log into, and every step below it is a convenience. # # Most of these live in the lionheartp/Hyprland COPR rather than Fedora proper. HYPR_FILE="$PANAMA_PATH/setup/packages/hyprland-packages" if [[ -f "$HYPR_FILE" ]]; then - log "Enabling Hyprland COPR" - sudo dnf copr enable -y lionheartp/Hyprland > /dev/null + log "Configuring the reviewed Hyprland repository" + configure_hyprland_repository > /dev/null HYPR_PACKAGES=$(packages_in "$HYPR_FILE") log "Installing Hyprland desktop packages" echo -e "Includes the following packages:" @@ -336,37 +621,12 @@ install_bun install_claude_code install_codex -# Claude Desktop: Anthropic ships macOS and Windows only, so this is a community -# RPM built from the official release. Panama used to build it from source -- it -# was `panama app claude-desktop` -- because no repository carried it. Upstream -# publishes one now, which is strictly better: the result upgrades with every -# other package instead of needing a slow rebuild each time a version ships. -# -# The repository is added by upstream's own setup script rather than by writing -# the .repo file out here. A baseurl copied into this repository is a pin by -# another name, and that script is the part upstream keeps correct. -if rpm -q claude-desktop-extra >/dev/null 2>&1; then - log "Claude Desktop already installed" -else - if [[ ! -f /etc/yum.repos.d/claude-desktop.repo ]]; then - log "Adding the Claude Desktop repository..." - # Fetched to a file and then run, never piped into root: a pipe executes - # whatever the network answered with no chance to look, and this one is an - # unpinned script from a personal GitHub Pages site -- the least trusted - # thing this installer touches. The file is kept next to the run so what - # executed is still on disk to read afterwards. - claude_repo_script="$(mktemp -t claude-desktop-repo.XXXXXX.sh)" - if curl -fsSL https://patrickjaja.github.io/claude-desktop-extra/install-rpm.sh \ - -o "$claude_repo_script"; then - sudo bash "$claude_repo_script" > /dev/null 2>&1 \ - || log "Could not add the Claude Desktop repository (script kept at $claude_repo_script)" - else - log "Could not download the Claude Desktop repository script" - fi - fi - log "Installing Claude Desktop..." - sudo dnf install -y claude-desktop-extra > /dev/null \ - || { log "Claude Desktop install failed; skipping"; softly_failed+=("Claude Desktop"); } +# Claude Desktop remains optional. Panama never downloads its community setup +# script; only a repository an operator has already configured with the exact +# reviewed local key is eligible for installation. +if ! install_claude_desktop_if_trusted; then + log "Claude Desktop install failed; skipping" + softly_failed+=("Claude Desktop") fi # RustDesk: remote desktop. The flatpak cannot register the root-owned system @@ -398,15 +658,18 @@ FLATPAK_FILE="$PANAMA_PATH/setup/packages/flatpak-packages" if [[ -f "$FLATPAK_FILE" ]]; then FLATPAK_PACKAGES=$(packages_in "$FLATPAK_FILE") log "Adding Flathub remote" - soft "adding the Flathub remote" \ - sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - log "Installing Flatpak Packages" - echo -e "Includes the following packages:" - echo -e "$(<"$FLATPAK_FILE")" - # One ID renamed on Flathub must not cost the rest of the run; the desktop - # is already installed by this point and none of these is part of it. - soft "some Flatpak packages" sudo flatpak install -y flathub $FLATPAK_PACKAGES - log "Flatpak packages installed!" + if ensure_flathub_remote; then + log "Installing Flatpak Packages" + echo -e "Includes the following packages:" + echo -e "$(<"$FLATPAK_FILE")" + # One ID renamed on Flathub must not cost the rest of the run; the desktop + # is already installed by this point and none of these is part of it. + soft "some Flatpak packages" sudo flatpak install -y flathub $FLATPAK_PACKAGES + log "Flatpak packages installed!" + else + log "Flathub trust verification failed; Flatpak packages were not installed" + softly_failed+=("Flathub") + fi else log "Package list was not in specified path: $FLATPAK_FILE" fi @@ -451,8 +714,13 @@ install_extra_category() { fi if [[ -n "${flatpak_ids// /}" ]]; then log "Installing $name flatpaks: $flatpak_ids" - sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo > /dev/null - sudo flatpak install -y flathub $flatpak_ids > /dev/null || { log "Some $name flatpaks did not install"; softly_failed+=("$name flatpaks"); } + if ensure_flathub_remote; then + sudo flatpak install -y flathub $flatpak_ids > /dev/null \ + || { log "Some $name flatpaks did not install"; softly_failed+=("$name flatpaks"); } + else + log "Flathub trust verification failed; $name flatpaks were not installed" + softly_failed+=("$name flatpaks") + fi fi } diff --git a/tests/setup/package-provenance-contract b/tests/setup/package-provenance-contract index 9603843..eaf823a 100755 --- a/tests/setup/package-provenance-contract +++ b/tests/setup/package-provenance-contract @@ -74,6 +74,18 @@ snapshot_gpg_state() { } | 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" @@ -87,6 +99,11 @@ 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_user_flatpak="$(snapshot_file_state "$HOME/.local/share/flatpak/repo/config")" +before_bashrc="$(snapshot_file_state "$HOME/.bashrc")" # This must be the only production file sourced by the contract. # shellcheck source=../../setup/lib/artifact-provenance @@ -255,4 +272,447 @@ cp "$config" "$parser_fixture" printf 'BUN_ARMV7_URL=https://fixture.invalid/bun\n' >> "$parser_fixture" expect_failure load_installer_provenance "$parser_fixture" +# Repository setup runs from a fixture copy of the installer with every +# external command replaced. A contract failure can therefore inspect exact +# ordering and staged bytes without consulting or changing the host. +installer_fixture="$test_tmp/installer-fixture" +mkdir -p "$installer_fixture/setup/lib" "$installer_fixture/setup/provenance/keys" \ + "$installer_fixture/setup/scripts" +cp "$repo_dir/setup/lib/artifact-provenance" "$repo_dir/setup/lib/extras-catalog" \ + "$repo_dir/setup/lib/machine-role" "$installer_fixture/setup/lib/" +cp "$config" "$installer_fixture/setup/provenance/installers.conf" +cp "$repo_dir"/setup/provenance/keys/*.asc "$installer_fixture/setup/provenance/keys/" +sed '/^# --- The server path/,$d' "$repo_dir/setup/scripts/install-packages" \ + > "$installer_fixture/setup/scripts/install-packages" + +make_stub_commands() { + local case_root="$1" + mkdir -p "$case_root/bin" "$case_root/home" "$case_root/tmp" "$case_root/etc/yum.repos.d" \ + "$case_root/etc/pki/rpm-gpg" + + 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 + printf 'rpm:query:%s\n' "${2:-}" >> "$COMMAND_LOG" + case "${2:-}" in + terra-release) [[ "${STUB_TERRA_INSTALLED:-0}" == 1 ]] ;; + claude-desktop-extra) [[ "${STUB_CLAUDE_DESKTOP_INSTALLED:-0}" == 1 ]] ;; + *) 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" +case "$url" in + *rpmfusion-free*) cp "$SIGNED_RPM" "$output" ;; + *rpmfusion-nonfree*) cp "$SIGNED_RPM" "$output" ;; + *flathub.flatpakrepo) cp "$FLATHUB_DESCRIPTOR" "$output" ;; + *) exit 66 ;; +esac +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 ]] || 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" + 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) ;; + flathub-key.asc) logged+=(FLATHUB_KEY) ;; + *) logged+=("$argument") ;; + esac +done +printf 'sudo:%s\n' "${logged[*]}" >> "$COMMAND_LOG" +if [[ -n "${STUB_DNF_FAIL_MATCH:-}" && "$original" == *"$STUB_DNF_FAIL_MATCH"* ]]; then + exit 68 +fi +if [[ "${1:-}" == flatpak && "${2:-}" == remote-add ]]; then + printf 'mutated\n' > "$STUB_FLATPAK_STATE" +fi +STUB + + for command in dnf flatpak; do + cat > "$case_root/bin/$command" <<'STUB' +#!/usr/bin/env bash +exit 69 +STUB + done + 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" + rm -rf -- "$case_root" + make_stub_commands "$case_root" + : > "$case_root/commands.log" + printf 'preserved\n' > "$case_root/flatpak-state" + 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 ]]; 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 + 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" \ + 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" \ + HOME="$case_root/home" \ + TMPDIR="$case_root/tmp" \ + PANAMA_PATH="$installer_fixture" \ + PATH="$case_root/bin:/usr/bin:/bin" \ + bash -c 'source "$PANAMA_PATH/setup/scripts/install-packages"; PANAMA_SYSTEM_ETC="$STUB_ETC"; declare -F "$1" >/dev/null; "$1"' \ + 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 + } +} + +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 +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 +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' + +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/ +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 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 +)" + +# 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_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' +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_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'