Install ChatGPT Desktop from a repository this checkout can verify

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
This commit is contained in:
Gabriel Brown
2026-08-27 14:48:20 -04:00
parent 1ee42f2cb6
commit cb305f6662
11 changed files with 456 additions and 23 deletions
-19
View File
@@ -1,19 +0,0 @@
# ChatGPT Desktop.
#
# OpenAI ships macOS and Windows only. This is a community wrapper that converts
# the upstream macOS disk image into a Linux Electron app and packages it as an
# RPM, so the installed result is again something dnf owns.
#
# Same exception, same reason: there is no packaged form to prefer. Nothing is
# pinned; `bootstrap-native` fetches the current upstream image each time and
# fails loudly when it cannot.
description="ChatGPT Desktop, built into a Fedora RPM"
repo="https://github.com/ilysenko/codex-desktop-linux.git"
# bootstrap-native installs build dependencies, builds, packages, and installs
# the newest artifact -- so unlike the Claude build there is no separate install
# step to do here.
build() {
make bootstrap-native
}
+38
View File
@@ -0,0 +1,38 @@
# Pinned signing keys
A key lands here when a publisher signs what Panama installs but does not
publish the key, or its fingerprint, anywhere an install could fetch and check
them first. Pinning the key is what lets `dnf` verify a download before root
ever sees it.
Nothing here is a secret. These are public keys, and the reason to track them
is that a *changed* one should be a merge request somebody reads, not a silent
change of who is trusted.
## `RPM-GPG-KEY-chatgpt`
| | |
| --- | --- |
| Fingerprint | `3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4` |
| User ID | `Codex Linux Repository` |
| Signs | the `chatgpt` package and the repository metadata at `https://persistent.oaistatic.com/codex-app-prod/linux/rpm/$basearch` |
| Used by | `setup/lib/chatgpt-package` |
Captured on 2026-08-27 from a machine where the official package had been
installed, at `/etc/pki/rpm-gpg/RPM-GPG-KEY-chatgpt`, where the package's own
root scriptlet writes it. It is the key that signed both the installed
`chatgpt` package and the live `repodata/repomd.xml.asc`.
Be honest about what that is worth: OpenAI's documented instructions
(<https://learn.chatgpt.com/docs/linux/linux-app>) are to download an RPM and
install it, and they publish no key URL and no fingerprint to compare against.
So this is trust established on first use and then held, not trust verified
against the publisher. Held is the part that matters -- from here every machine
checks the same fingerprint, and a swapped download fails instead of installing.
To re-derive the fingerprint from the file:
```bash
gpg --show-keys --with-colons setup/keys/RPM-GPG-KEY-chatgpt \
| awk -F: '$1 == "fpr" { print $10; exit }'
```
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGpypFUBEACi1Vvzq9pIpA6lj7chbqELuxJtVuzUzxrasa6ZU0yF4yhq7jf8
3YkJRHwbezBKeQyzJ5lkX0EhXS8aXxUhMAm3PFpAlwcInfKzmV7atJwvaxIw6Rmd
GYe9fBWKjTN/SmPIjtyxrTznZY97+TfD1AeGZpLaJ8fsnhrC+HkiN2TACiTocgpe
hFiP0OWK7mWZeTWnY2scpIYXP1Ro7nQv4KacmY4JacTQ7m/HM0Qej/3olhuEv2Cw
lMVWw57/oHhmTllfLDQOogFQyIVqaaR98y/Eu6cAabSfcsqAAZ2A8vfHYD27z28J
vLO2PZEJd5ThlnX4Zqv0eIpZdBj//8Sl/MSqTshFZ1NDsRoqwdqw284X5MpnOJ4k
4Sc2Se8tJxt/nCeibH3dJ504Fb1X/mnOqhCAQ6pVJz4RB5HRlFPSkxVPyag1v1m/
7T4vie+OR4eqFQNz6mudrOoMmeVIfyL5fbe4cOr4fk/FyvEE2xMgkFatPqXn7vM9
og+zremPCfwRAFpBPyX74VowFY7llcdaj/w8K5T8PzM14Hb3E4ZKizMluKmTvTq9
WE1/eSQJLLQqXD5VmtmdUaC/VyE/1ZlIxcA1LWqvEQ327UXREvX/nHsrkKrl956W
jzkiHFUTsD1NJ0dMfs+csOt8Furb5jZj+HsMmCm9jLdfz5b/4WKLPbvxIwARAQAB
tBZDb2RleCBMaW51eCBSZXBvc2l0b3J5iQJRBBMBCgA7FiEEO/oOSui4zBai2bpo
SjtKVmxGYOQFAmpypFUCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ
SjtKVmxGYORlCQ/9FyikZo8HQcJBP9E/oXVPds/fQnIFB2qJR2z3DrfYEonNt/ev
SAySkPPq4/mEOjaI0pFlDDGSaps+FTcJFgoVRTasBIF7JJivvjW9ap8iWEbhhVLe
IrFLbMLpUcTRntUx7R4fVMJ/1/cGn+NWZmNwS9ORorzSyCH0IAgCw1Xc3ZrjuMbF
VjdToMC1TiXXCEmlYpQakmQ3Ay1cH0FHC2BBNn1MNVkJdPhpZIZCdhaMPHfYFpyo
pg8wFvZ5iIcvlbMgyuy8CPJVRWUcYy2dOhEOGnYJnXRPkE3E1hf8YOHNzRlduH89
6lT9qcEK2+fpLfrVGoc4zscLZ+Ey+Ko6iQRdVE1j67+wNR3hX8ukue574v1N/xxu
i575jumSE19lEj1sH4+P4gFHOtTbF0JhKKzLctbga0IAwTPKhnt3qzj1U5Yj/MZS
uEVjrLhdRauOuFBXUclgyVf2w/lE85UUOdlcollsYA6Huq7xDamqf8SslZQGre3E
I+lhpqJR1cOwDMUzzcl40uTyhrxXXd/bk4QSlhZbwHR25Pnt+ZMtWavlQWS0eDEV
8djuXAURCmx5WOqAFB/TJe1mn5EvyWg4VFzrY/NVNOpzgY5+Xp7J28z7f637r712
Eu9j4imVcdPigwS+jf/0f81i2o9b82Y26TN8+EtDLCY841MJ1lrjDrX/dno=
=Y+3h
-----END PGP PUBLIC KEY BLOCK-----
+97
View File
@@ -0,0 +1,97 @@
# 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
}
+27
View File
@@ -79,6 +79,11 @@ source "$PANAMA_PATH/setup/lib/extras-catalog"
source "$PANAMA_PATH/setup/lib/machine-role"
ROLE="$(panama_role)"
# Establishing the verified ChatGPT repository, shared with the migration that
# replaces the community build, so neither can install it a less careful way.
# shellcheck source=../lib/chatgpt-package
source "$PANAMA_PATH/setup/lib/chatgpt-package"
# One list, installed the way every list is installed: --skip-unavailable so a
# single rotted name cannot cost the transaction, then report_missing so a
# skipped name is a warning somebody reads.
@@ -369,6 +374,28 @@ else
|| { log "Claude Desktop install failed; skipping"; softly_failed+=("Claude Desktop"); }
fi
# ChatGPT Desktop: OpenAI ships an official Linux RPM now. Panama used to build
# a community wrapper from the macOS disk image -- it was `panama app
# chatgpt-desktop` -- because no packaged form existed; that build froze often
# and carried its own local rebuild daemon. The official package is strictly
# better: it comes from a repository, so it upgrades with every other package
# from then on.
#
# The repository and its signing key are established first, from the copy
# pinned in setup/keys/, so dnf verifies the metadata and the package before
# either reaches root. Upstream's own instructions do not allow that -- see
# setup/lib/chatgpt-package for why they are not followed here.
if rpm -q chatgpt >/dev/null 2>&1; then
log "ChatGPT Desktop already installed"
elif ! chatgpt_install_repository sudo; then
log "Could not establish the verified ChatGPT repository; skipping"
softly_failed+=("ChatGPT Desktop")
else
log "Installing ChatGPT Desktop..."
sudo dnf install -y chatgpt > /dev/null \
|| { log "ChatGPT Desktop install failed; skipping"; softly_failed+=("ChatGPT Desktop"); }
fi
# RustDesk: remote desktop. The flatpak cannot register the root-owned system
# service that unattended access needs -- see panama-doctor's rustdesk check --
# so this takes the RPM. The download URL is resolved from the latest release