152 lines
5.8 KiB
Bash
Executable File
152 lines
5.8 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="" gpg_output
|
|
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
home="$(mktemp -u -d -t panama-gpg.XXXXXX)" || exit 1
|
|
if ! mkdir -m 700 -- "$home"; then
|
|
home=""
|
|
exit 1
|
|
fi
|
|
gpg_output="$(GNUPGHOME="$home" gpg --batch --with-colons \
|
|
--import-options show-only --import "$1" 2>/dev/null)" || exit 1
|
|
awk -F: '$1 == "pub" { primary = 1; next } primary && $1 == "fpr" { print $10; primary = 0 }' \
|
|
<<<"$gpg_output"
|
|
)
|
|
|
|
key_fingerprint_matches() {
|
|
local file="$1" expected="$2" 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=""
|
|
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
home="$(mktemp -u -d -t panama-gpg.XXXXXX)" || exit 1
|
|
if ! mkdir -m 700 -- "$home"; then
|
|
home=""
|
|
exit 1
|
|
fi
|
|
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 -u "$directory/.${filename}.part.XXXXXX")" || exit 1
|
|
if ! (set -o noclobber; : >"$part") 2>/dev/null; then
|
|
part=""
|
|
exit 1
|
|
fi
|
|
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
|
|
trap '[[ -z "$home" ]] || rm -rf -- "$home"' EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
key_fingerprint_matches "$key" "$expected" || exit 1
|
|
home="$(mktemp -u -d -t panama-rpm-signature.XXXXXX)" || exit 1
|
|
if ! mkdir -m 700 -- "$home"; then
|
|
home=""
|
|
exit 1
|
|
fi
|
|
db="$home/rpmdb"
|
|
mkdir -m 700 "$db" || exit 1
|
|
rpmkeys --dbpath "$db" --import "$key" >/dev/null 2>&1 || exit 1
|
|
output="$(rpmkeys --dbpath "$db" --checksig --verbose "$package" 2>&1)" \
|
|
|| exit 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 \
|
|
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
|
|
}
|