Fix: Verify third-party package repositories

This commit is contained in:
Gabriel Brown
2026-08-27 06:27:35 -04:00
parent 3f8539176a
commit 4abf3f8acd
2 changed files with 792 additions and 64 deletions
+332 -64
View File
@@ -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
}