Fix: Harden pinned runtime activation

This commit is contained in:
Gabriel Brown
2026-08-27 08:32:44 -04:00
parent 8264ca6320
commit 31fc1d3eaa
5 changed files with 501 additions and 95 deletions
+6 -6
View File
@@ -96,12 +96,12 @@ load_installer_provenance() {
local -A parsed=() allowed=() required_values=()
for name in \
BUN_VERSION BUN_X86_64_URL BUN_X86_64_SHA256 BUN_X86_64_MAX_BYTES \
BUN_AARCH64_URL BUN_AARCH64_SHA256 BUN_AARCH64_MAX_BYTES \
NODE_VERSION NODE_X86_64_URL NODE_X86_64_SHA256 NODE_X86_64_MAX_BYTES \
NODE_AARCH64_URL NODE_AARCH64_SHA256 NODE_AARCH64_MAX_BYTES \
CODEX_VERSION CODEX_X86_64_URL CODEX_X86_64_SHA256 CODEX_X86_64_MAX_BYTES \
CODEX_AARCH64_URL CODEX_AARCH64_SHA256 CODEX_AARCH64_MAX_BYTES \
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 \
+8
View File
@@ -64,6 +64,14 @@ Artifact digests were checked with `sha256sum ARTIFACT` against the lowercase
64-hex values in `installers.conf`; all runtime downloads also enforce the
recorded byte limit before atomic replacement.
The `*_BINARY_SHA256` values are the SHA-256 digests of the exact executable
bytes selected from each verified runtime archive. They were derived without
executing the artifacts: `tar -xO` selected Node's `bin/node` and Codex's
`bin/codex`, while `unzip -p` selected Bun's `bun`. Runtime version directories
record both the reviewed archive digest and this reviewed installed-byte digest
in `.panama-provenance`; later runs require the receipt and re-hash the installed
binary before treating an existing directory as trusted.
## Rotation policy
Do not replace a key on an automated update. A key rotation is a reviewed
+6
View File
@@ -1,23 +1,29 @@
BUN_VERSION=1.4.0
BUN_X86_64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip
BUN_X86_64_SHA256=2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452
BUN_X86_64_BINARY_SHA256=33d56b070be6a9e3da0ab013038b43d1645d0534ca811ecdba4472599117eb4b
BUN_X86_64_MAX_BYTES=67108864
BUN_AARCH64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip
BUN_AARCH64_SHA256=4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e
BUN_AARCH64_BINARY_SHA256=086c4121c8738a8e0f5ed730e8a461bc3973b4444e372ddb77aef9a747fa2ae9
BUN_AARCH64_MAX_BYTES=67108864
NODE_VERSION=24.20.0
NODE_X86_64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz
NODE_X86_64_SHA256=2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2
NODE_X86_64_BINARY_SHA256=89af8424dd53e560b1933f87ba650d8bf57c83ca5a04600eefb31f416aabbae7
NODE_X86_64_MAX_BYTES=67108864
NODE_AARCH64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz
NODE_AARCH64_SHA256=5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7
NODE_AARCH64_BINARY_SHA256=23a5637c2470fde09fcc1acc77c1b92e04e3d7e3e6e80ff7df6f5831958d1477
NODE_AARCH64_MAX_BYTES=67108864
CODEX_VERSION=0.150.1
CODEX_X86_64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz
CODEX_X86_64_SHA256=00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17
CODEX_X86_64_BINARY_SHA256=abf1bb1643a79f73aa78ee627e111e02d4f8c98f25813a0cf6ce277709664386
CODEX_X86_64_MAX_BYTES=134217728
CODEX_AARCH64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz
CODEX_AARCH64_SHA256=1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639
CODEX_AARCH64_BINARY_SHA256=7a49aabe11fd95a1c968d79e16b5f1b17c3219002c5f7129d467f415f8460feb
CODEX_AARCH64_MAX_BYTES=134217728
RUSTDESK_VERSION=1.4.9
RUSTDESK_X86_64_URL=https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm
+163 -44
View File
@@ -126,12 +126,17 @@ _set_artifact_arch() {
esac
}
_archive_path_is_safe() {
local member="$1"
[[ -n "$member" && "$member" != /* && "$member" != *'//'*
&& ! "$member" =~ (^|/)\.\.?(/|$) ]]
}
_archive_member_is_safe() {
local member="$1" expected_top="$2"
[[ -n "$member" && "$member" != /* && "$member" != *'//'*
&& ! "$member" =~ (^|/)\.\.?(/|$)
&& ( "$member" == "$expected_top" || "$member" == "$expected_top/" \
|| "$member" == "$expected_top/"* ) ]]
_archive_path_is_safe "$member"
[[ "$member" == "$expected_top" || "$member" == "$expected_top/" \
|| "$member" == "$expected_top/"* ]]
}
_tree_links_stay_inside() {
@@ -142,17 +147,59 @@ _tree_links_stay_inside() {
done < <(find "$root" -type l -print0)
}
_atomic_symlink() {
local target="$1" destination="$2" directory temporary
_tree_hardlinks_stay_inside() {
local root="$1" device inode link_count key
local -A names_in_tree=() inode_links=()
while read -r device inode link_count; do
key="$device:$inode"
names_in_tree["$key"]=$(( ${names_in_tree[$key]:-0} + 1 ))
inode_links["$key"]="$link_count"
done < <(find "$root" -type f -printf '%D %i %n\n')
for key in "${!names_in_tree[@]}"; do
[[ "${names_in_tree[$key]}" == "${inode_links[$key]}" ]] || return 1
done
}
_atomic_symlink() (
local target="$1" destination="$2" directory temporary=""
trap '[[ -z "$temporary" ]] || rm -f -- "$temporary"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
directory="$(dirname -- "$destination")"
mkdir -p -- "$directory" || return 1
temporary="$(mktemp "$directory/.$(basename -- "$destination").link.XXXXXX")" || return 1
rm -f -- "$temporary" || return 1
ln -s -- "$target" "$temporary" || return 1
if ! mv -Tf -- "$temporary" "$destination"; then
rm -f -- "$temporary"
return 1
fi
)
_activate_directory_no_replace() {
local staged="$1" destination="$2"
mv -Tn -- "$staged" "$destination" || return 1
[[ ! -e "$staged" && ! -L "$staged" && -d "$destination" && ! -L "$destination" ]]
}
_write_runtime_receipt() {
local directory="$1" artifact_digest="$2" binary_digest="$3"
local receipt="$directory/.panama-provenance"
[[ -d "$directory" && ! -L "$directory" && ! -e "$receipt" && ! -L "$receipt" ]] \
|| return 1
( umask 077 && printf 'schema=1\nartifact_sha256=%s\nbinary_sha256=%s\n' \
"$artifact_digest" "$binary_digest" > "$receipt" )
}
_runtime_receipt_matches() {
local directory="$1" binary="$2" artifact_digest="$3" binary_digest="$4"
local receipt="$directory/.panama-provenance" actual
[[ -d "$directory" && ! -L "$directory"
&& -f "$receipt" && ! -L "$receipt"
&& -f "$binary" && ! -L "$binary" ]] || return 1
cmp -s "$receipt" <(printf 'schema=1\nartifact_sha256=%s\nbinary_sha256=%s\n' \
"$artifact_digest" "$binary_digest") || return 1
actual="$(sha256sum "$binary" | awk '{ print $1 }')" || return 1
[[ "$actual" == "$binary_digest" ]]
}
_load_nvm() {
@@ -165,8 +212,13 @@ _load_nvm() {
declare -F nvm >/dev/null
}
_install_node() {
_install_node() (
local artifact_arch machine_arch archive_top parent target stage archive extract listing member
local artifact_digest binary_digest staged_binary
stage=""
trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
_set_artifact_arch || return 1
_load_nvm || return 1
case "$artifact_arch" in
@@ -176,8 +228,12 @@ _install_node() {
archive_top="node-v${INSTALLER_PROVENANCE[NODE_VERSION]}-linux-$machine_arch"
parent="${NVM_DIR:-$HOME/.nvm}/versions/node"
target="$parent/v${INSTALLER_PROVENANCE[NODE_VERSION]}"
artifact_digest="${INSTALLER_PROVENANCE[NODE_${artifact_arch}_SHA256]}"
binary_digest="${INSTALLER_PROVENANCE[NODE_${artifact_arch}_BINARY_SHA256]}"
if [[ -e "$target" || -L "$target" ]]; then
[[ -d "$target" && ! -L "$target" && -x "$target/bin/node"
_runtime_receipt_matches "$target" "$target/bin/node" \
"$artifact_digest" "$binary_digest" || return 1
[[ -x "$target/bin/node"
&& "$($target/bin/node --version 2>/dev/null)" == "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|| return 1
nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1 || return 1
@@ -204,17 +260,34 @@ _install_node() {
done <<<"$listing"
tar -xJf "$archive" --no-same-owner --no-same-permissions -C "$extract" \
|| { rm -rf -- "$stage"; return 1; }
[[ -d "$extract/$archive_top" && ! -L "$extract/$archive_top"
&& -x "$extract/$archive_top/bin/node"
&& "$($extract/$archive_top/bin/node --version 2>/dev/null)" \
== "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
_tree_links_stay_inside "$extract/$archive_top" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$extract/$archive_top" "$target" || { rm -rf -- "$stage"; return 1; }
_tree_hardlinks_stay_inside "$extract/$archive_top" \
|| { rm -rf -- "$stage"; return 1; }
staged_binary="$extract/$archive_top/bin/node"
[[ "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest" ]] \
|| { rm -rf -- "$stage"; return 1; }
[[ -d "$extract/$archive_top" && ! -L "$extract/$archive_top"
&& -x "$staged_binary"
&& "$($staged_binary --version 2>/dev/null)" \
== "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
_write_runtime_receipt "$extract/$archive_top" "$artifact_digest" "$binary_digest" \
|| { rm -rf -- "$stage"; return 1; }
_activate_directory_no_replace "$extract/$archive_top" "$target" \
|| { rm -rf -- "$stage"; return 1; }
_runtime_receipt_matches "$target" "$target/bin/node" \
"$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; }
[[ -x "$target/bin/node"
&& "$($target/bin/node --version 2>/dev/null)" \
== "v${INSTALLER_PROVENANCE[NODE_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
_tree_links_stay_inside "$target" || { rm -rf -- "$stage"; return 1; }
_tree_hardlinks_stay_inside "$target" || { rm -rf -- "$stage"; return 1; }
rm -rf -- "$stage"
stage=""
nvm alias default "${INSTALLER_PROVENANCE[NODE_VERSION]}" >/dev/null 2>&1
}
)
install_node() {
_install_node || _record_installer_failure Node
@@ -226,15 +299,20 @@ setup_node() {
}
install_pnpm() {
if sudo dnf install -y pnpm >/dev/null; then
if require_reviewed_fedora_release \
&& sudo dnf install -y --repo=fedora --repo=updates pnpm >/dev/null; then
return 0
fi
_record_installer_failure pnpm
}
_install_bun() {
_install_bun() (
local artifact_arch archive_top target bin_link parent stage archive listing
local staged_binary version_dir
local staged_binary version_dir artifact_digest binary_digest
stage=""
trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
_set_artifact_arch || return 1
case "$artifact_arch" in
X86_64) archive_top=bun-linux-x64 ;;
@@ -243,8 +321,12 @@ _install_bun() {
version_dir="$HOME/.bun/versions/${INSTALLER_PROVENANCE[BUN_VERSION]}"
target="$version_dir/bin/bun"
bin_link="$HOME/.bun/bin/bun"
artifact_digest="${INSTALLER_PROVENANCE[BUN_${artifact_arch}_SHA256]}"
binary_digest="${INSTALLER_PROVENANCE[BUN_${artifact_arch}_BINARY_SHA256]}"
if [[ -e "$version_dir" || -L "$version_dir" ]]; then
[[ -d "$version_dir" && ! -L "$version_dir" && -x "$target"
_runtime_receipt_matches "$version_dir" "$target" \
"$artifact_digest" "$binary_digest" || return 1
[[ -x "$target"
&& "$($target --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|| return 1
[[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \
@@ -264,22 +346,35 @@ _install_bun() {
return 1
fi
listing="$(unzip -Z1 "$archive")" || { rm -rf -- "$stage"; return 1; }
[[ "$listing" == "$archive_top/bun" ]] || { rm -rf -- "$stage"; return 1; }
_archive_member_is_safe "$listing" "$archive_top" \
[[ "$listing" == "$archive_top/"$'\n'"$archive_top/bun" ]] \
|| { rm -rf -- "$stage"; return 1; }
while IFS= read -r member; do
_archive_member_is_safe "$member" "$archive_top" \
|| { rm -rf -- "$stage"; return 1; }
done <<<"$listing"
mkdir -m 0700 "$stage/extract" "$stage/version" "$stage/version/bin" \
|| { rm -rf -- "$stage"; return 1; }
unzip -q "$archive" -d "$stage/extract" || { rm -rf -- "$stage"; return 1; }
staged_binary="$stage/extract/$archive_top/bun"
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary"
&& "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest"
&& "$($staged_binary --version 2>/dev/null)" == "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$staged_binary" "$stage/version/bin/bun" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$stage/version" "$version_dir" || { rm -rf -- "$stage"; return 1; }
_write_runtime_receipt "$stage/version" "$artifact_digest" "$binary_digest" \
|| { rm -rf -- "$stage"; return 1; }
_activate_directory_no_replace "$stage/version" "$version_dir" \
|| { rm -rf -- "$stage"; return 1; }
_runtime_receipt_matches "$version_dir" "$target" \
"$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; }
[[ -x "$target" && "$($target --version 2>/dev/null)" \
== "${INSTALLER_PROVENANCE[BUN_VERSION]}" ]] \
|| { rm -rf -- "$stage"; return 1; }
rm -rf -- "$stage"
stage=""
_atomic_symlink "$target" "$bin_link"
}
)
install_bun() {
_install_bun || _record_installer_failure Bun
@@ -292,20 +387,23 @@ _codex_version_matches() {
[[ "$output" =~ (^|[^0-9])${version_pattern}([^0-9]|$) ]]
}
_install_codex() {
local artifact_arch machine_arch archive_name version_dir target bin_link parent
local stage archive listing staged_binary
_install_codex() (
local artifact_arch version_dir target bin_link parent expected_listing member
local stage archive listing staged_binary artifact_digest binary_digest
stage=""
trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
_set_artifact_arch || return 1
case "$artifact_arch" in
X86_64) machine_arch=x86_64 ;;
AARCH64) machine_arch=aarch64 ;;
esac
archive_name="codex-$machine_arch-unknown-linux-musl"
version_dir="$HOME/.local/lib/panama/codex/${INSTALLER_PROVENANCE[CODEX_VERSION]}"
target="$version_dir/codex"
bin_link="$HOME/.local/bin/codex"
artifact_digest="${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_SHA256]}"
binary_digest="${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_BINARY_SHA256]}"
if [[ -e "$version_dir" || -L "$version_dir" ]]; then
[[ -d "$version_dir" && ! -L "$version_dir" && -x "$target" ]] || return 1
_runtime_receipt_matches "$version_dir" "$target" \
"$artifact_digest" "$binary_digest" || return 1
[[ -x "$target" ]] || return 1
_codex_version_matches "$target" || return 1
[[ -L "$bin_link" && "$(readlink -- "$bin_link")" == "$target" ]] \
|| _atomic_symlink "$target" "$bin_link"
@@ -324,31 +422,47 @@ _install_codex() {
return 1
fi
listing="$(tar -tzf "$archive")" || { rm -rf -- "$stage"; return 1; }
[[ "$listing" == "$archive_name" ]] || { rm -rf -- "$stage"; return 1; }
[[ "$listing" != /* && ! "$listing" =~ (^|/)\.\.?(/|$) ]] \
|| { rm -rf -- "$stage"; return 1; }
expected_listing=$'bin/\nbin/codex\nbin/codex-code-mode-host\ncodex-package.json\ncodex-path/\ncodex-path/rg\ncodex-resources/\ncodex-resources/bwrap\ncodex-resources/zsh/\ncodex-resources/zsh/bin/\ncodex-resources/zsh/bin/zsh'
[[ "$listing" == "$expected_listing" ]] || { rm -rf -- "$stage"; return 1; }
while IFS= read -r member; do
_archive_path_is_safe "$member" || { rm -rf -- "$stage"; return 1; }
done <<<"$listing"
mkdir -m 0700 "$stage/extract" "$stage/version" \
|| { rm -rf -- "$stage"; return 1; }
tar -xzf "$archive" --no-same-owner --no-same-permissions -C "$stage/extract" \
|| { rm -rf -- "$stage"; return 1; }
staged_binary="$stage/extract/$archive_name"
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary" ]] \
_tree_links_stay_inside "$stage/extract" || { rm -rf -- "$stage"; return 1; }
_tree_hardlinks_stay_inside "$stage/extract" || { rm -rf -- "$stage"; return 1; }
staged_binary="$stage/extract/bin/codex"
[[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary"
&& "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest" ]] \
|| { rm -rf -- "$stage"; return 1; }
_codex_version_matches "$staged_binary" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$staged_binary" "$stage/version/codex" \
|| { rm -rf -- "$stage"; return 1; }
mv -- "$stage/version" "$version_dir" || { rm -rf -- "$stage"; return 1; }
_write_runtime_receipt "$stage/version" "$artifact_digest" "$binary_digest" \
|| { rm -rf -- "$stage"; return 1; }
_activate_directory_no_replace "$stage/version" "$version_dir" \
|| { rm -rf -- "$stage"; return 1; }
_runtime_receipt_matches "$version_dir" "$target" \
"$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; }
_codex_version_matches "$target" || { rm -rf -- "$stage"; return 1; }
rm -rf -- "$stage"
stage=""
_atomic_symlink "$target" "$bin_link"
}
)
install_codex() {
_install_codex || _record_installer_failure Codex
}
_install_rustdesk() {
_install_rustdesk() (
local artifact_arch installed_version="" work rpm_path status=0
work=""
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
_set_artifact_arch || return 1
if [[ "$artifact_arch" == AARCH64 ]]; then
log "RustDesk ${INSTALLER_PROVENANCE[RUSTDESK_VERSION]} has no reviewed aarch64 RPM"
@@ -367,10 +481,14 @@ _install_rustdesk() {
rm -rf -- "$work"
return 1
fi
sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$rpm_path" >/dev/null || status=$?
# RustDesk 1.4.9's reviewed RPM is unsigned. Its exact SHA-256 is the trust
# assertion; this exception applies only to the verified private local file
# and does not change signature policy for any repository.
sudo dnf install -y --setopt=localpkg_gpgcheck=0 "$rpm_path" >/dev/null || status=$?
rm -rf -- "$work"
work=""
return "$status"
}
)
install_rustdesk() {
_install_rustdesk || _record_installer_failure RustDesk
@@ -945,7 +1063,8 @@ _install_claude_code() {
"$staged_key" /etc/pki/rpm-gpg/RPM-GPG-KEY-claude-code-panama \
"$staged_repo" /etc/yum.repos.d/claude-code.repo || status=$?
if (( status == 0 )); then
sudo dnf install -y claude-code || status=$?
sudo dnf install -y --repo=claude-code --repo=fedora --repo=updates \
claude-code || status=$?
fi
rm -rf -- "$work"
return "$status"