feat(packaging/bazzite): the sysext feed is signed, and the client refuses one that isn't
`punktfunk-sysext` checked the SHA256 of every image it downloaded, which sounds like verification but isn't: SHA256SUMS lives on the same registry as the images it describes, so anything able to replace an image could replace its checksum in the same request. The checksum only ever proved the download wasn't corrupted. Each feed now carries SHA256SUMS.asc, a detached OpenPGP signature over the manifest, and the client verifies it before believing a line of it. The key is packages@unom.io (AF245C506F4E4763) — the same one that already signs our RPMs, so boxes have one key to trust and we have one key to rotate. Its public half is baked into the script rather than fetched from the feed, because a key you fetch from the thing you're authenticating authenticates nothing; gpg (present on Bazzite) does the verifying against a throwaway keyring holding only that key, so "good signature" and "signed by us" are the same statement. Rollout: stable feeds only publish on a tag, so a `--seal` mode re-signs an existing manifest without rebuilding an image, and every rpm.yml run seals the OTHER channel of its Fedora major too. Canary pushes are frequent, so all live feeds seal within a day of this landing and a key rotation propagates without republishing anything. Until a feed is sealed the client refuses it and says so, naming PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1 as the informed way through. Two things testing changed. The baked-key fingerprint check compared against an empty string — the armor block is a single-quoted shell literal, so the extracted range carried `FEED_KEY='` on its first line and gpg saw no armor at all; every publish would have "mismatched" and, on a tag, failed the release. And `status` captured fetch_manifest's stderr but only printed it on failure, swallowing the ALLOW_UNSIGNED warning precisely when someone was running unverified. Verified end to end on Bazzite 44 against a file:// feed with a throwaway key: unsigned feed refused; ALLOW_UNSIGNED=1 proceeds and says so; wrong signer rejected; tampered manifest rejected; good signature accepted; `update` exits 1 before touching anything on a bad feed. Publisher side: signs when the baked key matches, refuses on mismatch, refuses with no key, and its signature round-trips through the real client. shellcheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,8 @@ sudo bash punktfunk-sysext.sh install
|
||||
```
|
||||
|
||||
This downloads the newest image for your Fedora base (host + tray + **web console**,
|
||||
SHA-256-verified from the feed `…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/`),
|
||||
SHA-256-verified against a signed manifest from the feed
|
||||
`…/packages/unom/generic/punktfunk-sysext/f<ver>[-canary]/`),
|
||||
installs it as `/var/lib/extensions/punktfunk.raw`, merges it, and immediately applies what the
|
||||
RPM scriptlets would have (udev reload, sysctl) plus the two `/etc` files a sysext can't carry
|
||||
(the gamescope-session drop-in and the tray autostart entry, staged under
|
||||
@@ -63,6 +64,14 @@ sudo punktfunk-sysext remove # unmerge + delete; ~/.config/punktfunk is left
|
||||
|
||||
Details worth knowing:
|
||||
|
||||
- **The feed is signed.** Each feed carries `SHA256SUMS` plus a detached OpenPGP signature
|
||||
`SHA256SUMS.asc` from `packages@unom.io` (`AF245C506F4E4763`) — the same key that signs our RPMs.
|
||||
`punktfunk-sysext` verifies that signature, with the public key baked into the script, *before*
|
||||
it believes the manifest, and refuses a feed it can't verify. The checksums alone never proved
|
||||
authorship: they sit on the same registry as the images they describe, so whatever could replace
|
||||
an image could replace its checksum in the same breath. If you are on a feed published before
|
||||
signing existed, it is sealed on the next publish to that Fedora major; to install from it
|
||||
meanwhile — accepting that the image is unauthenticated — set `PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1`.
|
||||
- The image embeds `ID=fedora` + `VERSION_ID` (matched through Bazzite's `ID_LIKE`), so after a
|
||||
**major Bazzite rebase** (F43 → F44) the old image is **refused** instead of merging
|
||||
soname-broken binaries — `punktfunk-sysext update` then fetches the image built for the new
|
||||
|
||||
@@ -1,31 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish a punktfunk sysext image into its feed on the Gitea generic package registry —
|
||||
# called by .gitea/workflows/rpm.yml after the RPM publish. A feed is one fixed URL
|
||||
# (…/punktfunk-sysext/<feed>/) holding versioned .raw files plus a SHA256SUMS manifest;
|
||||
# punktfunk-sysext(8) on the boxes reads SHA256SUMS to find + verify the newest image
|
||||
# (the layout is also exactly what systemd-sysupdate's url-file source expects, so a
|
||||
# .transfer feed can be added later without re-publishing anything).
|
||||
# (…/punktfunk-sysext/<feed>/) holding versioned .raw files plus a SHA256SUMS manifest and its
|
||||
# detached OpenPGP signature SHA256SUMS.asc; punktfunk-sysext(8) on the boxes verifies that
|
||||
# signature, then uses the manifest to find + check the newest image (the layout is also exactly
|
||||
# what systemd-sysupdate's url-file source expects, so a .transfer feed can be added later
|
||||
# without re-publishing anything).
|
||||
#
|
||||
# Signing uses RPM_GPG_PRIVATE_KEY — the SAME packages@unom.io key that signs the RPMs, so boxes
|
||||
# have one key to trust and we have one key to rotate. Without checksums-plus-signature the feed
|
||||
# was self-certifying: SHA256SUMS sits on the same registry as the images it describes, so whoever
|
||||
# could swap an image could swap its checksum in the same breath.
|
||||
#
|
||||
# Usage: TOKEN=… [KEEP=6] bash publish-sysext-feed.sh <feed> <image.raw>
|
||||
# TOKEN=… bash publish-sysext-feed.sh --seal <feed>
|
||||
# <feed> e.g. f43, f43-canary, f44 (Fedora major x channel)
|
||||
# KEEP newest images to keep in the feed; 0/unset-for-stable = keep all
|
||||
# Env: REGISTRY (git.unom.io), OWNER (unom), TOKEN (write:package PAT), CURL_USER (login name)
|
||||
# --seal re-sign a feed's EXISTING manifest without publishing an image. For feeds published
|
||||
# before signing existed, and after a key rotation. Idempotent.
|
||||
# Env: REGISTRY (git.unom.io), OWNER (unom), TOKEN (write:package PAT), CURL_USER (login name),
|
||||
# RPM_GPG_PRIVATE_KEY (armored private key; absent => unsigned, fatal on a v* tag)
|
||||
set -euo pipefail
|
||||
|
||||
FEED="${1:?usage: publish-sysext-feed.sh <feed> <image.raw>}"
|
||||
RAW="${2:?usage: publish-sysext-feed.sh <feed> <image.raw>}"
|
||||
[ -f "$RAW" ] || { echo "no such image: $RAW" >&2; exit 1; }
|
||||
SEAL=0
|
||||
if [ "${1:-}" = --seal ]; then
|
||||
SEAL=1; FEED="${2:?usage: publish-sysext-feed.sh --seal <feed>}"; RAW=""
|
||||
else
|
||||
FEED="${1:?usage: publish-sysext-feed.sh <feed> <image.raw>}"
|
||||
RAW="${2:?usage: publish-sysext-feed.sh <feed> <image.raw>}"
|
||||
[ -f "$RAW" ] || { echo "no such image: $RAW" >&2; exit 1; }
|
||||
fi
|
||||
REGISTRY="${REGISTRY:-git.unom.io}"
|
||||
OWNER="${OWNER:-unom}"
|
||||
KEEP="${KEEP:-0}"
|
||||
AUTH=(--user "${CURL_USER:-enricobuehler}:${TOKEN:?TOKEN (write:package PAT) required}")
|
||||
BASE="https://$REGISTRY/api/packages/$OWNER/generic/punktfunk-sysext/$FEED"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
SUMS="$WORK/SHA256SUMS"
|
||||
SIG="$WORK/SHA256SUMS.asc"
|
||||
|
||||
# sign_manifest — detached-sign $SUMS into $SIG with RPM_GPG_PRIVATE_KEY. Prints nothing and
|
||||
# returns 1 if no key is available; the caller decides whether that is survivable.
|
||||
sign_manifest() {
|
||||
[ -n "${RPM_GPG_PRIVATE_KEY:-}" ] || return 1
|
||||
local home keyid baked
|
||||
home="$(mktemp -d)"; chmod 700 "$home"
|
||||
# Loopback pinentry for the same reason sign-rpms.sh needs it: no TTY in CI.
|
||||
printf 'pinentry-mode loopback\n' > "$home/gpg.conf"
|
||||
printf 'allow-loopback-pinentry\n' > "$home/gpg-agent.conf"
|
||||
printf '%s' "$RPM_GPG_PRIVATE_KEY" | GNUPGHOME="$home" gpg --batch --quiet --import
|
||||
keyid="$(GNUPGHOME="$home" gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/{print $10; exit}')"
|
||||
# A feed signed by a key the client script doesn't carry is a feed nobody can install from, and
|
||||
# the failure would only surface on someone's Bazzite box. Compare fingerprints here instead: the
|
||||
# baked-in public key in punktfunk-sysext.sh must be the key we are about to sign with.
|
||||
# The armor block is a shell single-quoted literal in that script, so the range's first and last
|
||||
# lines carry the `FEED_KEY='` prefix and the closing quote — strip them or gpg sees no armor at
|
||||
# all and hands back an empty fingerprint, which would look like a mismatch on every publish.
|
||||
baked="$(sed -n '/BEGIN PGP PUBLIC KEY BLOCK/,/END PGP PUBLIC KEY BLOCK/p' "$HERE/punktfunk-sysext.sh" \
|
||||
| sed "s/^FEED_KEY='//; s/'\$//" \
|
||||
| GNUPGHOME="$home" gpg --batch --quiet --with-colons --import-options show-only --import 2>/dev/null \
|
||||
| awk -F: '/^fpr:/{print $10; exit}')"
|
||||
if [ -z "$baked" ] || [ "$keyid" != "$baked" ]; then
|
||||
echo "signing key $keyid does not match the key baked into punktfunk-sysext.sh ($baked)" >&2
|
||||
echo "-> rotate BOTH (packaging/rpm/README.md), or clients cannot verify this feed." >&2
|
||||
rm -rf "$home"; return 1
|
||||
fi
|
||||
GNUPGHOME="$home" gpg --batch --yes --armor --detach-sign --local-user "$keyid" \
|
||||
--output "$SIG" "$SUMS"
|
||||
rm -rf "$home"
|
||||
echo "signed SHA256SUMS with $keyid"
|
||||
}
|
||||
|
||||
# require_signature — on a v* tag an unsigned feed is a hard stop, exactly like sign-rpms.sh:
|
||||
# clients refuse unsigned feeds, so publishing one would strand every box on the stable channel.
|
||||
require_signature() {
|
||||
case "${GITHUB_REF:-}" in
|
||||
refs/tags/v*)
|
||||
echo "release build (${GITHUB_REF}) but the sysext feed could not be signed — aborting." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
# Deliberately mode-neutral wording: in --seal mode nothing is published at all, and a log line
|
||||
# claiming otherwise is the kind of thing that costs an hour six months from now.
|
||||
echo "WARNING: $FEED left UNSIGNED (no usable RPM_GPG_PRIVATE_KEY); clients will refuse it." >&2
|
||||
}
|
||||
|
||||
# --seal: re-sign whatever manifest the feed already has, no image, no pruning.
|
||||
if [ "$SEAL" = 1 ]; then
|
||||
curl -fsS "${AUTH[@]}" -o "$SUMS" "$BASE/SHA256SUMS" \
|
||||
|| { echo "no SHA256SUMS at $BASE — nothing to seal" >&2; exit 1; }
|
||||
sign_manifest || require_signature
|
||||
if [ -f "$SIG" ]; then
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/SHA256SUMS.asc" || true
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" --upload-file "$SIG" "$BASE/SHA256SUMS.asc"
|
||||
echo "sealed $BASE ($(wc -l <"$SUMS") image(s))"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FNAME="$(basename "$RAW")"
|
||||
SHA="$(sha256sum "$RAW" | cut -d' ' -f1)"
|
||||
|
||||
# Merge into the existing manifest: drop any prior line for this filename, append ours.
|
||||
SUMS="$(mktemp)"; trap 'rm -f "$SUMS"' EXIT
|
||||
curl -fsS "${AUTH[@]}" "$BASE/SHA256SUMS" 2>/dev/null | grep -v " $FNAME\$" > "$SUMS" || true
|
||||
printf '%s %s\n' "$SHA" "$FNAME" >> "$SUMS"
|
||||
|
||||
@@ -38,13 +115,24 @@ if [ "$KEEP" -gt 0 ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload order keeps consumers consistent: image first, then the manifest referencing it,
|
||||
# then prune deletions (already absent from the manifest). Delete-before-put makes workflow
|
||||
# re-runs idempotent (the registry 409s on duplicate filenames; first-publish 404s are fine).
|
||||
# Sign the finished manifest BEFORE anything is uploaded — a signing failure on a release must
|
||||
# abort while the live feed is still whole, not halfway through being replaced.
|
||||
sign_manifest || require_signature
|
||||
|
||||
# Upload order keeps consumers consistent: image first, then the manifest referencing it, then its
|
||||
# signature, then prune deletions (already absent from the manifest). A client that catches the
|
||||
# window between the manifest and its signature sees an unsigned feed and refuses — it does not see
|
||||
# a manifest vouched for by a stale signature, which is why the .asc goes last and never first.
|
||||
# Delete-before-put makes workflow re-runs idempotent (the registry 409s on duplicate filenames;
|
||||
# first-publish 404s are fine).
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/$FNAME" || true
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" --upload-file "$RAW" "$BASE/$FNAME"
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/SHA256SUMS" || true
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" --upload-file "$SUMS" "$BASE/SHA256SUMS"
|
||||
if [ -f "$SIG" ]; then
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/SHA256SUMS.asc" || true
|
||||
curl -fsS -o /dev/null "${AUTH[@]}" --upload-file "$SIG" "$BASE/SHA256SUMS.asc"
|
||||
fi
|
||||
for f in "${PRUNE[@]:-}"; do
|
||||
[ -n "$f" ] && { echo "pruning $f"; curl -fsS -o /dev/null "${AUTH[@]}" -X DELETE "$BASE/$f" || true; }
|
||||
done
|
||||
|
||||
@@ -16,10 +16,17 @@
|
||||
# sudo punktfunk-sysext update | status | remove
|
||||
#
|
||||
# Feed: the Gitea generic package registry, one feed per Fedora major x channel
|
||||
# (…/punktfunk-sysext/f43/, f43-canary, f44, …), each a SHA256SUMS + versioned .raw files —
|
||||
# published by .gitea/workflows/rpm.yml from the same RPMs the (legacy) layering path uses.
|
||||
# The image pins ID=fedora + VERSION_ID, so after a major OS rebase the old image is refused
|
||||
# (…/punktfunk-sysext/f43/, f43-canary, f44, …), each a SHA256SUMS + SHA256SUMS.asc + versioned
|
||||
# .raw files — published by .gitea/workflows/rpm.yml from the same RPMs the (legacy) layering path
|
||||
# uses. The image pins ID=fedora + VERSION_ID, so after a major OS rebase the old image is refused
|
||||
# (not merged broken) and `punktfunk-sysext update` re-resolves against the new release.
|
||||
#
|
||||
# Trust: SHA256SUMS carries a detached OpenPGP signature (SHA256SUMS.asc) from packages@unom.io —
|
||||
# the same key that signs our RPMs — and this script verifies it before believing a word of the
|
||||
# manifest. The checksums alone could never have done that: they live on the same registry as the
|
||||
# images they describe, so anything able to replace an image could replace its checksum too. The
|
||||
# public key is baked in below rather than fetched, because a key fetched from the thing you are
|
||||
# authenticating authenticates nothing.
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="${PUNKTFUNK_SYSEXT_REGISTRY:-https://git.unom.io/api/packages/unom/generic/punktfunk-sysext}"
|
||||
@@ -29,6 +36,21 @@ IMG="$EXT_DIR/punktfunk.raw"
|
||||
SIDECAR="$EXT_DIR/.punktfunk.version"
|
||||
MARKER=/usr/lib/extension-release.d/extension-release.punktfunk
|
||||
ETC_SRC=/usr/share/punktfunk/etc
|
||||
PF_TMP="$(mktemp -d)"; trap 'rm -rf "$PF_TMP"' EXIT
|
||||
|
||||
# The feed's signing key: punktfunk packages <packages@unom.io>, AF245C506F4E4763. Identical to
|
||||
# packaging/rpm/RPM-GPG-KEY-punktfunk — ONE key signs both the RPMs and this feed, so rotating it
|
||||
# means updating both copies (the rotation runbook in packaging/rpm/README.md says so, and
|
||||
# publish-sysext-feed.sh refuses to sign if the two ever disagree).
|
||||
FEED_KEY='-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEai/2eRYJKwYBBAHaRw8BAQdAFxLGvh8wvzES9ylmxT4gy1i58EituotPyZwt
|
||||
z+y9rbC0JXB1bmt0ZnVuayBwYWNrYWdlcyA8cGFja2FnZXNAdW5vbS5pbz6IkAQT
|
||||
FgoAOBYhBDG6uOY81eoQ6beahK8kXFBvTkdjBQJqL/Z5AhsjBQsJCAcCBhUKCQgL
|
||||
AgQWAgMBAh4BAheAAAoJEK8kXFBvTkdj1QsBAM0sI/qUzGEbuC2Zrk36QQBrUu/9
|
||||
sy5uhYGZD6lMJ4uZAQC7W81H2gHlTDTA2Nq35HKW9IOU+Ll2c9fqa7fAIKf9Bg==
|
||||
=e4Az
|
||||
-----END PGP PUBLIC KEY BLOCK-----'
|
||||
|
||||
usage() {
|
||||
sed -n 's/^#\( \|$\)//p' "$0" | sed -n '1,20p'
|
||||
@@ -47,12 +69,61 @@ feed_url() {
|
||||
echo "$REGISTRY/f$(os_version_id)$suffix"
|
||||
}
|
||||
|
||||
# latest -> "VERSION FILENAME SHA256" from the feed's SHA256SUMS (highest by version sort).
|
||||
# verify_manifest SUMS SIG -> 0 iff SIG is a good detached signature over SUMS by FEED_KEY.
|
||||
# A throwaway keyring holding exactly our one key, so "good signature" and "signed by us" are the
|
||||
# same statement — any other signer comes back NO_PUBKEY, and gpg exits non-zero.
|
||||
# Spelled out with plain `if`s rather than `cond && action`: under `set -e` a failing test at the
|
||||
# end of an && list is a trap that only bites on the path nobody exercises (here: a corrupt
|
||||
# FEED_KEY), and this function must never abort the script — its whole job is to return a verdict.
|
||||
verify_manifest() {
|
||||
local home rc=1
|
||||
home="$(mktemp -d)"; chmod 700 "$home"
|
||||
if printf '%s\n' "$FEED_KEY" | GNUPGHOME="$home" gpg --batch --quiet --import 2>/dev/null; then
|
||||
if GNUPGHOME="$home" gpg --batch --quiet --verify "$2" "$1" 2>/dev/null; then rc=0; fi
|
||||
fi
|
||||
rm -rf "$home"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# fetch_manifest -> download the feed's SHA256SUMS into $PF_TMP and verify its signature.
|
||||
# Returns non-zero (having said why) rather than exiting, so `status` can report a bad feed
|
||||
# instead of dying on it; install/update turn that into a hard stop.
|
||||
fetch_manifest() {
|
||||
local feed sums sig
|
||||
feed="$(feed_url)"
|
||||
sums="$PF_TMP/SHA256SUMS"; sig="$PF_TMP/SHA256SUMS.asc"
|
||||
curl -fsSL -o "$sums" "$feed/SHA256SUMS" || { echo "cannot reach the feed $feed" >&2; return 1; }
|
||||
if [ "${PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED:-0}" = 1 ]; then
|
||||
echo "!! PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1 — the feed manifest is NOT being verified." >&2
|
||||
return 0
|
||||
fi
|
||||
# curl's own "404"/"could not open file" is noise here — a missing signature is an expected
|
||||
# state with a much better explanation below, so swallow it and say the useful thing instead.
|
||||
if ! curl -fsSL -o "$sig" "$feed/SHA256SUMS.asc" 2>/dev/null; then
|
||||
echo "!! the feed $feed has no SHA256SUMS.asc — refusing to install from an unsigned feed." >&2
|
||||
echo "!! (a feed published before signing existed; it is sealed on the next publish. To install" >&2
|
||||
echo "!! from it anyway, knowing the images are unauthenticated: PUNKTFUNK_SYSEXT_ALLOW_UNSIGNED=1)" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! command -v gpg >/dev/null 2>&1; then
|
||||
echo "!! gpg not found — cannot verify the feed signature. Install gnupg2." >&2
|
||||
return 1
|
||||
fi
|
||||
if ! verify_manifest "$sums" "$sig"; then
|
||||
echo "!! the feed's SHA256SUMS is NOT signed by packages@unom.io (AF245C506F4E4763)." >&2
|
||||
echo "!! Someone has tampered with the feed, or the signing key was rotated and this script is" >&2
|
||||
echo "!! older than the rotation. Do not install; re-download punktfunk-sysext.sh and retry." >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# latest -> "VERSION FILENAME SHA256" for the newest image in the VERIFIED manifest (version sort).
|
||||
# Call fetch_manifest first — reading $PF_TMP/SHA256SUMS directly is what keeps the signature
|
||||
# check off the subshell path, where an `exit` would have vanished into a command substitution.
|
||||
latest() {
|
||||
local feed; feed="$(feed_url)"
|
||||
curl -fsSL "$feed/SHA256SUMS" \
|
||||
| awk '$2 ~ /^punktfunk-.*-x86-64\.raw$/ { v=$2; sub(/^punktfunk-/,"",v); sub(/-x86-64\.raw$/,"",v); print v, $2, $1 }' \
|
||||
| sort -V | tail -n1
|
||||
awk '$2 ~ /^punktfunk-.*-x86-64\.raw$/ { v=$2; sub(/^punktfunk-/,"",v); sub(/-x86-64\.raw$/,"",v); print v, $2, $1 }' \
|
||||
"$PF_TMP/SHA256SUMS" | sort -V | tail -n1
|
||||
}
|
||||
|
||||
installed_version() {
|
||||
@@ -156,6 +227,7 @@ cmd_install() {
|
||||
if [ -n "$from_file" ]; then
|
||||
do_install --from-file "$from_file"
|
||||
else
|
||||
fetch_manifest || exit 1
|
||||
local l; l="$(latest)"
|
||||
[ -n "$l" ] || { echo "no image in the feed $(feed_url)" >&2; exit 1; }
|
||||
# shellcheck disable=SC2086
|
||||
@@ -178,6 +250,7 @@ cmd_update() {
|
||||
if [ "${1:-}" = --from-file ]; then do_install --from-file "${2:?}"; return; fi
|
||||
local cur l ver
|
||||
cur="$(installed_version)"
|
||||
fetch_manifest || exit 1
|
||||
l="$(latest)"
|
||||
[ -n "$l" ] || { echo "no image in the feed $(feed_url)" >&2; exit 1; }
|
||||
ver="${l%% *}"
|
||||
@@ -197,7 +270,17 @@ cmd_status() {
|
||||
echo "image: $([ -f "$IMG" ] && du -h "$IMG" | cut -f1 || echo '(not installed)')"
|
||||
echo "merged: $(merged && echo yes || echo no)"
|
||||
echo "installed: $(installed_version || true)"
|
||||
echo "latest: $(latest 2>/dev/null | cut -d' ' -f1 || true)"
|
||||
# Say WHY the feed is unreadable rather than printing a blank: unreachable and
|
||||
# "signature does not verify" want very different reactions from whoever ran this.
|
||||
if fetch_manifest 2>"$PF_TMP/status.err"; then
|
||||
echo "latest: $(latest | cut -d' ' -f1)"
|
||||
else
|
||||
echo "latest: (unavailable)"
|
||||
fi
|
||||
# Unconditionally — fetch_manifest also warns on SUCCESS (ALLOW_UNSIGNED), and a status command
|
||||
# that hides "this feed is not being verified" is worse than one that prints nothing at all.
|
||||
[ -s "$PF_TMP/status.err" ] && sed 's/^/ /' "$PF_TMP/status.err" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
|
||||
@@ -88,6 +88,15 @@ curl --user "<user>:<write:package-PAT>" --upload-file packaging/rpm/RPM-GPG-KEY
|
||||
Rotating the key means a new generic-registry version (bump `punktfunk-keys/1` → `/2` and the
|
||||
`gpgkey=` URL), since the registry rejects re-uploading an existing file.
|
||||
|
||||
**This key also signs the Bazzite sysext feed**, and a third copy of its public half is baked into
|
||||
`packaging/bazzite/punktfunk-sysext.sh` (`FEED_KEY=`) — that script is bootstrapped by `curl` on
|
||||
machines that have nothing installed yet, so it can't fetch the key from the thing it's
|
||||
authenticating. A rotation must update **all three**: the CI secret, this directory's
|
||||
`RPM-GPG-KEY-punktfunk` (+ its registry upload), and `FEED_KEY`. `publish-sysext-feed.sh` compares
|
||||
its signing key's fingerprint against `FEED_KEY` and refuses to sign on a mismatch, so forgetting
|
||||
the third one fails the publish instead of stranding every Bazzite box in front of a feed it
|
||||
can't verify.
|
||||
|
||||
After reboot, as the desktop user:
|
||||
|
||||
```sh
|
||||
|
||||
Reference in New Issue
Block a user