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:
@@ -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
|
||||
}
|
||||
@@ -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 }'
|
||||
```
|
||||
@@ -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-----
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user