Fix: Verify third-party package repositories
This commit is contained in:
+332
-64
@@ -67,6 +67,15 @@ softly_failed=()
|
|||||||
# PANAMA_PATH, and clobbering it here made a clone anywhere else source the
|
# PANAMA_PATH, and clobbering it here made a clone anywhere else source the
|
||||||
# extras catalog from a path that does not exist.
|
# extras catalog from a path that does not exist.
|
||||||
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
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
|
# Reading the extras catalog, shared with `panama apps` so the two front doors
|
||||||
# cannot disagree about what a category contains.
|
# cannot disagree about what a category contains.
|
||||||
@@ -152,17 +161,6 @@ install_bun() {
|
|||||||
fi
|
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
|
# 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.
|
# setup_node -- the nvm-managed Node is the one it should land in.
|
||||||
install_codex() {
|
install_codex() {
|
||||||
@@ -195,6 +193,297 @@ report_soft_failures() {
|
|||||||
fi
|
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 ---------------------------------------------------------
|
# --- The server path ---------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Everything a server runs is above this line plus the lists it installs. No
|
# Everything a server runs is above this line plus the lists it installs. No
|
||||||
@@ -217,7 +506,7 @@ fi
|
|||||||
|
|
||||||
echo -e "\n--- Installing Repositories ---"
|
echo -e "\n--- Installing Repositories ---"
|
||||||
log "Installing RPM Fusion Free and Nonfree 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"
|
log "Enabling Fedora Cisco OpenH264 Repository"
|
||||||
# soft: this repo does not exist on every spin, and its absence must not cost
|
# 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
|
# 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
|
# in the repository section, above everything, so `set -e` ended the stage
|
||||||
# before a single package was considered. An installer whose second run does
|
# before a single package was considered. An installer whose second run does
|
||||||
# less than its first is worse than one that never ran.
|
# less than its first is worse than one that never ran.
|
||||||
if rpm -q terra-release >/dev/null 2>&1; then
|
log "Installing Terra Repository"
|
||||||
log "Terra repository already installed"
|
install_terra_repository > /dev/null
|
||||||
else
|
|
||||||
log "Installing Terra Repository"
|
|
||||||
sudo dnf install -y --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release > /dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n--- Installing relevant packages ---"
|
echo -e "\n--- Installing relevant packages ---"
|
||||||
log "Updating all packages. This may take a while"
|
log "Updating all packages. This may take a while"
|
||||||
@@ -258,16 +543,16 @@ install_list desktop-packages "Desktop"
|
|||||||
|
|
||||||
# --- Install the Hyprland desktop ---
|
# --- Install the Hyprland desktop ---
|
||||||
#
|
#
|
||||||
# Directly after desktop-packages, which is what supplies the dnf plugin that
|
# Directly after desktop-packages and deliberately before anything optional.
|
||||||
# `dnf copr` needs, and deliberately before anything optional. This is the one
|
# 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
|
# 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.
|
# 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.
|
# Most of these live in the lionheartp/Hyprland COPR rather than Fedora proper.
|
||||||
HYPR_FILE="$PANAMA_PATH/setup/packages/hyprland-packages"
|
HYPR_FILE="$PANAMA_PATH/setup/packages/hyprland-packages"
|
||||||
if [[ -f "$HYPR_FILE" ]]; then
|
if [[ -f "$HYPR_FILE" ]]; then
|
||||||
log "Enabling Hyprland COPR"
|
log "Configuring the reviewed Hyprland repository"
|
||||||
sudo dnf copr enable -y lionheartp/Hyprland > /dev/null
|
configure_hyprland_repository > /dev/null
|
||||||
HYPR_PACKAGES=$(packages_in "$HYPR_FILE")
|
HYPR_PACKAGES=$(packages_in "$HYPR_FILE")
|
||||||
log "Installing Hyprland desktop packages"
|
log "Installing Hyprland desktop packages"
|
||||||
echo -e "Includes the following packages:"
|
echo -e "Includes the following packages:"
|
||||||
@@ -336,37 +621,12 @@ install_bun
|
|||||||
install_claude_code
|
install_claude_code
|
||||||
install_codex
|
install_codex
|
||||||
|
|
||||||
# Claude Desktop: Anthropic ships macOS and Windows only, so this is a community
|
# Claude Desktop remains optional. Panama never downloads its community setup
|
||||||
# RPM built from the official release. Panama used to build it from source -- it
|
# script; only a repository an operator has already configured with the exact
|
||||||
# was `panama app claude-desktop` -- because no repository carried it. Upstream
|
# reviewed local key is eligible for installation.
|
||||||
# publishes one now, which is strictly better: the result upgrades with every
|
if ! install_claude_desktop_if_trusted; then
|
||||||
# other package instead of needing a slow rebuild each time a version ships.
|
log "Claude Desktop install failed; skipping"
|
||||||
#
|
softly_failed+=("Claude Desktop")
|
||||||
# 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"); }
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# RustDesk: remote desktop. The flatpak cannot register the root-owned system
|
# 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
|
if [[ -f "$FLATPAK_FILE" ]]; then
|
||||||
FLATPAK_PACKAGES=$(packages_in "$FLATPAK_FILE")
|
FLATPAK_PACKAGES=$(packages_in "$FLATPAK_FILE")
|
||||||
log "Adding Flathub remote"
|
log "Adding Flathub remote"
|
||||||
soft "adding the Flathub remote" \
|
if ensure_flathub_remote; then
|
||||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
log "Installing Flatpak Packages"
|
||||||
log "Installing Flatpak Packages"
|
echo -e "Includes the following packages:"
|
||||||
echo -e "Includes the following packages:"
|
echo -e "$(<"$FLATPAK_FILE")"
|
||||||
echo -e "$(<"$FLATPAK_FILE")"
|
# One ID renamed on Flathub must not cost the rest of the run; the desktop
|
||||||
# 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.
|
||||||
# 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
|
||||||
soft "some Flatpak packages" sudo flatpak install -y flathub $FLATPAK_PACKAGES
|
log "Flatpak packages installed!"
|
||||||
log "Flatpak packages installed!"
|
else
|
||||||
|
log "Flathub trust verification failed; Flatpak packages were not installed"
|
||||||
|
softly_failed+=("Flathub")
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log "Package list was not in specified path: $FLATPAK_FILE"
|
log "Package list was not in specified path: $FLATPAK_FILE"
|
||||||
fi
|
fi
|
||||||
@@ -451,8 +714,13 @@ install_extra_category() {
|
|||||||
fi
|
fi
|
||||||
if [[ -n "${flatpak_ids// /}" ]]; then
|
if [[ -n "${flatpak_ids// /}" ]]; then
|
||||||
log "Installing $name flatpaks: $flatpak_ids"
|
log "Installing $name flatpaks: $flatpak_ids"
|
||||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo > /dev/null
|
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"); }
|
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ snapshot_gpg_state() {
|
|||||||
} | sha256sum | awk '{ print $1 }'
|
} | 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"
|
export GNUPGHOME="$ambient_gnupg"
|
||||||
mkdir -m 700 "$ambient_gnupg"
|
mkdir -m 700 "$ambient_gnupg"
|
||||||
printf 'baseline-value\n' > "$ambient_gnupg/unexpected-entry"
|
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_gnupg="$(snapshot "$host_gnupg")"
|
||||||
before_gpg_files="$(snapshot_gpg_state "$host_gnupg")"
|
before_gpg_files="$(snapshot_gpg_state "$host_gnupg")"
|
||||||
before_rpmdb="$(snapshot "$host_rpmdb")"
|
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.
|
# This must be the only production file sourced by the contract.
|
||||||
# shellcheck source=../../setup/lib/artifact-provenance
|
# 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"
|
printf 'BUN_ARMV7_URL=https://fixture.invalid/bun\n' >> "$parser_fixture"
|
||||||
expect_failure load_installer_provenance "$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'
|
printf 'package provenance contract: PASS\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user