The repository audit made any Terra that is not Panama's own pinned form a trust-root failure, and status 78 then stopped every stage before it ran. A machine that installed Terra the way Terra documents it -- terra-release's own repo file, a metalink, the key at its stock path -- was classified hostile and had no way back, because install_terra_repository refused to touch a machine terra-release had already reached. A gate with no door. The trust root is the signing key, and that key is byte-for-byte the fingerprint this repository reviewed and pinned, with every signature check already on. So verify the fingerprint and adopt the configuration into the pinned form instead of refusing it. Adoption needs no network and no DNF, it runs before any other transaction in the stage, and it is repeatable, which it has to be: terra-release owns that file and restores it on update. Adoption stays narrow. The pinned fingerprint must match both the reviewed key and the key the machine actually verifies against, the gpgkey must be a local file under the system trust directory, and the endpoint must be one Terra itself serves -- so the reviewed baseurl or the reviewed metalink host, now pinned as TERRA_METALINK_BASEURL. An unknown key, a redirected baseurl, a second enabled Terra, or a disabled signature check is still a hard refusal. A refusal also stops less than it did. It suppresses the stages that open DNF and the migrations, which may run a transaction of their own, and the run still exits 78. It no longer stops link-dotfiles, link-skills or link-user, which read no repository and install no package. Exiting before them is what left this laptop with a stale ~/.claude/skills and no shipped skill reachable. Also stub ensure_flathub_remote in the extras contract, which has been failing since that call was added to install_extra_category without one. Claude-Session: https://claude.ai/code/session_01PeTrG9dGY89UWuhGm4Pr1s
142 lines
5.4 KiB
Bash
Executable File
142 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Reviewed installer inputs are data, never shell code. Consumers source this
|
|
# library, load the data through its parser, and verify each fetched artifact
|
|
# before any privileged operation can see it.
|
|
|
|
declare -gA INSTALLER_PROVENANCE=()
|
|
|
|
_primary_key_fingerprints() (
|
|
local home
|
|
set -o pipefail
|
|
home="$(mktemp -d)" || exit 1
|
|
chmod 700 "$home"
|
|
trap 'rm -rf -- "$home"' EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
GNUPGHOME="$home" gpg --batch --with-colons --import-options show-only --import "$1" 2>/dev/null \
|
|
| awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }'
|
|
)
|
|
|
|
key_fingerprint_matches() {
|
|
local file="$1" expected="$2"
|
|
local output
|
|
local -a primary_fingerprints=()
|
|
output="$(_primary_key_fingerprints "$file")" || return 1
|
|
[[ -n "$output" ]] || return 1
|
|
mapfile -t primary_fingerprints <<<"$output"
|
|
[[ ${#primary_fingerprints[@]} -eq 1 && "${primary_fingerprints[0]}" == "$expected" ]]
|
|
}
|
|
|
|
_key_has_one_primary() {
|
|
local output
|
|
local -a primary_fingerprints=()
|
|
output="$(_primary_key_fingerprints "$1")" || return 1
|
|
[[ -n "$output" ]] || return 1
|
|
mapfile -t primary_fingerprints <<<"$output"
|
|
[[ ${#primary_fingerprints[@]} -eq 1 ]]
|
|
}
|
|
|
|
verify_detached_signature() {
|
|
local key="$1" signature="$2" content="$3" home
|
|
_key_has_one_primary "$key" || return 1
|
|
(
|
|
home="$(mktemp -d)" || exit 1
|
|
chmod 700 "$home"
|
|
trap 'rm -rf -- "$home"' EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
GNUPGHOME="$home" gpg --batch --quiet --import "$key" >/dev/null 2>&1 \
|
|
&& GNUPGHOME="$home" gpg --batch --verify "$signature" "$content" >/dev/null 2>&1
|
|
)
|
|
}
|
|
|
|
download_sha256() {
|
|
local url="$1" expected="$2" max_bytes="$3" destination="$4" actual directory filename
|
|
|
|
[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || return 1
|
|
actual="$expected"
|
|
|
|
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]*$ ]] || exit 1
|
|
[[ -n "$destination" && -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" ]] || exit 1
|
|
[[ "$(stat -c %s "$part")" -le "$max_bytes" ]] || exit 1
|
|
[[ "$(sha256sum "$part" | awk '{ print $1 }')" == "$actual" ]] || exit 1
|
|
mv -f -- "$part" "$destination"
|
|
)
|
|
}
|
|
|
|
rpm_signature_matches() {
|
|
local package="$1" key="$2" expected="$3" home db output status
|
|
|
|
key_fingerprint_matches "$key" "$expected" || return 1
|
|
home="$(mktemp -d)" || return 1
|
|
chmod 700 "$home"
|
|
db="$home/rpmdb"
|
|
mkdir -m 700 "$db" || {
|
|
rm -rf -- "$home"
|
|
return 1
|
|
}
|
|
|
|
rpmkeys --dbpath "$db" --import "$key" >/dev/null 2>&1 \
|
|
&& output="$(rpmkeys --dbpath "$db" --checksig --verbose "$package" 2>&1)"
|
|
status=$?
|
|
rm -rf -- "$home"
|
|
(( status == 0 )) || return 1
|
|
grep -Eqi 'OpenPGP.*signature.*: OK' <<<"$output"
|
|
}
|
|
|
|
load_installer_provenance() {
|
|
local file="$1" line name value required
|
|
local -A parsed=() allowed=() required_values=()
|
|
|
|
for name in \
|
|
BUN_VERSION BUN_X86_64_URL BUN_X86_64_SHA256 BUN_X86_64_BINARY_SHA256 BUN_X86_64_MAX_BYTES \
|
|
BUN_AARCH64_URL BUN_AARCH64_SHA256 BUN_AARCH64_BINARY_SHA256 BUN_AARCH64_MAX_BYTES \
|
|
NODE_VERSION NODE_X86_64_URL NODE_X86_64_SHA256 NODE_X86_64_BINARY_SHA256 NODE_X86_64_MAX_BYTES \
|
|
NODE_AARCH64_URL NODE_AARCH64_SHA256 NODE_AARCH64_BINARY_SHA256 NODE_AARCH64_MAX_BYTES \
|
|
CODEX_VERSION CODEX_X86_64_URL CODEX_X86_64_SHA256 CODEX_X86_64_BINARY_SHA256 CODEX_X86_64_MAX_BYTES \
|
|
CODEX_AARCH64_URL CODEX_AARCH64_SHA256 CODEX_AARCH64_BINARY_SHA256 CODEX_AARCH64_MAX_BYTES \
|
|
RUSTDESK_VERSION RUSTDESK_X86_64_URL RUSTDESK_X86_64_SHA256 RUSTDESK_X86_64_MAX_BYTES \
|
|
FEDORA_RELEASE RPMFUSION_FREE_RELEASE_URL RPMFUSION_FREE_RELEASE_MAX_BYTES \
|
|
RPMFUSION_NONFREE_RELEASE_URL RPMFUSION_NONFREE_RELEASE_MAX_BYTES TERRA_BASEURL \
|
|
TERRA_METALINK_BASEURL \
|
|
HYPRLAND_COPR_BASEURL FLATHUB_DESCRIPTOR_URL FLATHUB_DESCRIPTOR_MAX_BYTES \
|
|
CLAUDE_CODE_BASEURL CLAUDE_DESKTOP_BASEURL TERRA_FINGERPRINT CLAUDE_CODE_FINGERPRINT \
|
|
BUN_FINGERPRINT RPMFUSION_FREE_FINGERPRINT RPMFUSION_NONFREE_FINGERPRINT \
|
|
HYPRLAND_COPR_FINGERPRINT FLATHUB_FINGERPRINT CLAUDE_DESKTOP_FINGERPRINT; do
|
|
allowed["$name"]=1
|
|
required_values["$name"]=1
|
|
done
|
|
|
|
[[ -r "$file" ]] || return 1
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
[[ "$line" == *=* && "${line#*=}" != *=* ]] || return 1
|
|
IFS='=' read -r name value <<<"$line"
|
|
[[ -n "${allowed[$name]:-}" && -n "$value" && -z "${parsed[$name]:-}" ]] || return 1
|
|
[[ "$value" != *'$('* && "$value" != *'`'* ]] || return 1
|
|
parsed["$name"]="$value"
|
|
done < "$file"
|
|
|
|
for required in "${!required_values[@]}"; do
|
|
[[ -n "${parsed[$required]:-}" ]] || return 1
|
|
done
|
|
|
|
INSTALLER_PROVENANCE=()
|
|
for name in "${!parsed[@]}"; do
|
|
INSTALLER_PROVENANCE["$name"]="${parsed[$name]}"
|
|
done
|
|
}
|