OpenAI ships an official Linux RPM now, so the community wrapper goes away: `panama app chatgpt-desktop` built codex-desktop from the upstream macOS disk image and ran a local rebuild daemon to keep it current, and the official package comes from a repository that upgrades with everything else. The app file, the help example and the dock's pinned id all move over, and a migration replaces the build on machines that already have it -- official package on before the community one comes off, so a failure part-way still leaves an app. The install itself does not follow upstream's instructions. Those are "download this RPM and install it", and the RPM's own root scriptlet is what writes the repository file and drops the signing key into /etc/pki/rpm-gpg -- so root runs an unverified download and then learns from it what to trust. That is the shape the repository audit forbids: no network response is executed as root without a verified digest or signature first. OpenAI publishes no key and no fingerprint anywhere an install could fetch and check them, so the key is pinned here instead. setup/keys/ carries it and says where it came from, including the honest part -- this is trust established on first use and then held, not trust verified against the publisher. setup/lib/ chatgpt-package verifies that copy's fingerprint, installs it, and writes the repository with gpgcheck and repo_gpgcheck on before anything is installed, so dnf checks the metadata signature and the package signature itself. It is byte for byte the repository the scriptlet would have written, so nothing churns afterwards, and every later upgrade goes through the same key. Both callers use it; a verification failure skips ChatGPT rather than installing it anyway. The contract proves the pinned key is the key the library names, that a missing, unreadable or mismatched key writes nothing at all, that what is written actually turns the checks on, and that neither caller hands root a downloaded RPM. Claude-Session: https://claude.ai/code/session_017zzbtfnMLoYrB8WesqANFY
98 lines
4.3 KiB
Plaintext
98 lines
4.3 KiB
Plaintext
# Installing OpenAI's ChatGPT Desktop without trusting the download. Sourced,
|
|
# not run.
|
|
#
|
|
# OpenAI signs both its packages and its repository metadata, with one key, and
|
|
# publishes neither that key nor its fingerprint anywhere a first install could
|
|
# fetch them. The documented instructions are "download this RPM and install
|
|
# it" -- and the RPM's own root scriptlet is what writes the repository file and
|
|
# drops the key into /etc/pki/rpm-gpg. Following them means handing an
|
|
# unverified download to root and letting it decide afterwards what to trust,
|
|
# which is the one thing this repository will not do with a network response.
|
|
#
|
|
# So the key is pinned here instead. setup/keys/ carries a copy and records
|
|
# where it came from; this verifies that copy's fingerprint, installs it, and
|
|
# writes the repository itself with gpgcheck on. dnf then checks the metadata
|
|
# signature and the package signature against that key before anything runs as
|
|
# root, and every later upgrade goes through the same repository and the same
|
|
# key.
|
|
#
|
|
# Two callers, which is why this is a library: install-packages, for a machine
|
|
# being built, and the migration that replaces the community codex-desktop
|
|
# build on machines that predate the official package.
|
|
|
|
# The key that signs the packages and the repository metadata. Pinned, so a
|
|
# substituted key is a failure here rather than a silent change of publisher.
|
|
CHATGPT_KEY_FINGERPRINT="3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4"
|
|
|
|
# `$basearch` stays literal: dnf expands it, and this is the same base URL the
|
|
# package's own scriptlet configures.
|
|
CHATGPT_REPO_BASEURL="https://persistent.oaistatic.com/codex-app-prod/linux/rpm/\$basearch"
|
|
CHATGPT_REPO_FILE="/etc/yum.repos.d/chatgpt.repo"
|
|
CHATGPT_KEY_FILE="/etc/pki/rpm-gpg/RPM-GPG-KEY-chatgpt"
|
|
|
|
chatgpt_pinned_key() {
|
|
printf '%s/setup/keys/RPM-GPG-KEY-chatgpt' "${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
|
}
|
|
|
|
# The fingerprint of the pinned copy. Nonzero when it cannot be read at all,
|
|
# which the caller reports differently from a key that reads but is the wrong
|
|
# one.
|
|
chatgpt_pinned_fingerprint() {
|
|
local key
|
|
key="$(chatgpt_pinned_key)"
|
|
[[ -r "$key" ]] || return 1
|
|
gpg --show-keys --with-colons "$key" 2>/dev/null \
|
|
| awk -F: '$1 == "fpr" { print $10; exit }'
|
|
}
|
|
|
|
# Fails without touching anything when the pinned key is missing, unreadable,
|
|
# or not the key this repository says it is. Everything below assumes it passed.
|
|
chatgpt_verify_pinned_key() {
|
|
local found
|
|
if ! command -v gpg >/dev/null 2>&1; then
|
|
printf 'gpg is missing, so the pinned ChatGPT signing key cannot be verified.\n' >&2
|
|
return 1
|
|
fi
|
|
if ! found="$(chatgpt_pinned_fingerprint)"; then
|
|
printf 'The pinned ChatGPT signing key is missing: %s\n' "$(chatgpt_pinned_key)" >&2
|
|
return 1
|
|
fi
|
|
if [[ "$found" != "$CHATGPT_KEY_FINGERPRINT" ]]; then
|
|
printf 'The pinned ChatGPT signing key is %s, not the expected %s.\n' \
|
|
"${found:-unreadable}" "$CHATGPT_KEY_FINGERPRINT" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Installs the verified key and the repository that names it, so the install
|
|
# after this one is a signature check rather than an act of faith.
|
|
#
|
|
# Takes the command that gets root, because the two callers ask for it
|
|
# differently: plain `sudo` from the installer, which authenticated once at the
|
|
# top of the run, and `panama-sudo --reason ...` from a migration, whose prompt
|
|
# has to say which repair it is for.
|
|
chatgpt_install_repository() {
|
|
local -a sudo_cmd=("$@")
|
|
(( ${#sudo_cmd[@]} > 0 )) || sudo_cmd=(sudo)
|
|
|
|
chatgpt_verify_pinned_key || return 1
|
|
|
|
"${sudo_cmd[@]}" install -D -m 0644 "$(chatgpt_pinned_key)" "$CHATGPT_KEY_FILE" || return 1
|
|
"${sudo_cmd[@]}" rpmkeys --import "$CHATGPT_KEY_FILE" || return 1
|
|
|
|
# Written here rather than left to the package's scriptlet, because the
|
|
# point of it is to exist -- with gpgcheck on and this key named -- before
|
|
# the first install rather than after it. Same base URL and same key the
|
|
# scriptlet writes, so it finds nothing to change later.
|
|
printf '%s\n' \
|
|
'[openai-chatgpt]' \
|
|
'name=ChatGPT' \
|
|
"baseurl=$CHATGPT_REPO_BASEURL" \
|
|
'enabled=1' \
|
|
'type=rpm-md' \
|
|
'gpgcheck=1' \
|
|
'repo_gpgcheck=1' \
|
|
"gpgkey=file://$CHATGPT_KEY_FILE" \
|
|
| "${sudo_cmd[@]}" tee "$CHATGPT_REPO_FILE" >/dev/null || return 1
|
|
}
|