Files
punktfunk/packaging/rpm/sign-rpms.sh
T
enricobuehlerandClaude Opus 5 7b053a4a39 fix(packaging/rpm): a release must not publish unsigned RPMs into a gpgcheck=1 repo
Signing was already live and the docs were half right about it. `RPM_GPG_PRIVATE_KEY`
is an ORG-level secret on unom, so it is invisible in this repo's Actions secrets —
which reads exactly like "never set up", and both the rpm.yml step name and
sign-rpms.sh's header still said "dormant". Checked it on the wire instead: a
published punktfunk-web RPM carries an OpenPGP V4 EdDSA header signature from
af245c506f4e4763, the same key committed at packaging/rpm/RPM-GPG-KEY-punktfunk.

The real gap was the failure mode. README.md hands users a repo file with
gpgcheck=1, but sign-rpms.sh exits 0 when the key is missing — so an org secret
that got rotated, renamed, or not inherited would publish an unsigned release into
a repo that rejects unsigned packages, and every user's `dnf upgrade` would break
with us none the wiser. On refs/tags/v* that is now a build failure. Other builds
still fall through unsigned so forks and local builds keep working.

Docs corrected to match: the org-level location (with a wire-level check that
doesn't depend on where the secret lives), the fail-closed rule, and a note that
`rpmkeys --checksig` reporting NOKEY still means signed.

Guard tested locally: exit 1 on refs/tags/v0.21.0, exit 0 on refs/heads/main and
on an unset ref.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:12:21 +02:00

58 lines
3.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Detached-GPG-sign the built dist/*.rpm so the Gitea RPM registry can be served with gpgcheck=1.
#
# ACTIVE: RPM_GPG_PRIVATE_KEY is set as an ORG-level secret on `unom` (packages@unom.io,
# AF245C506F4E4763 — the public half is committed at packaging/rpm/RPM-GPG-KEY-punktfunk and served
# from the registry). Every published RPM carries its EdDSA header signature; the repo file in
# README.md tells users gpgcheck=1, which only works because of that.
#
# Without the key this exits 0 and leaves the RPMs unsigned, so a fork or a local build still works
# — but NOT on a release. On refs/tags/v* a missing key is a hard failure: an org secret that got
# rotated, renamed, or not inherited would otherwise publish an unsigned release into a repo whose
# own instructions say gpgcheck=1, and every user's `dnf upgrade` would break on it. Better to fail
# the build than to find out from users. (Same fail-closed rule as the Windows pack scripts.)
#
# Requires a DEDICATED, PASSPHRASE-LESS signing key (the one the runbook generates with
# %no-protection), distinct from the Gitea instance's repo-metadata key — rpm's default signer
# can't supply a passphrase non-interactively here.
#
# Usage (in rpm.yml, after build-rpm.sh): RPM_GPG_PRIVATE_KEY=... bash packaging/rpm/sign-rpms.sh
set -euo pipefail
if [ -z "${RPM_GPG_PRIVATE_KEY:-}" ]; then
case "${GITHUB_REF:-}" in
refs/tags/v*)
echo "RPM_GPG_PRIVATE_KEY unset on a release build (${GITHUB_REF}) — refusing to publish" >&2
echo "unsigned RPMs into a gpgcheck=1 repo. Restore the org secret (packaging/rpm/README.md)." >&2
exit 1 ;;
esac
echo "RPM_GPG_PRIVATE_KEY unset — leaving dist/*.rpm UNSIGNED (non-release build)."
exit 0
fi
command -v rpmsign >/dev/null 2>&1 || dnf -y install rpm-sign >/dev/null
GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; chmod 700 "$GNUPGHOME"
trap 'rm -rf "$GNUPGHOME"' EXIT
# Non-interactive in CI (no TTY): force loopback pinentry via gpg.conf so even rpm's default
# signing macro's gpg call won't try to prompt. The passphrase-less key needs no prompt anyway.
printf 'pinentry-mode loopback\n' > "$GNUPGHOME/gpg.conf"
printf 'allow-loopback-pinentry\n' > "$GNUPGHOME/gpg-agent.conf"
printf '%s' "$RPM_GPG_PRIVATE_KEY" | gpg --batch --import
KEYID="$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}')"
[ -n "$KEYID" ] || { echo "no secret key imported from RPM_GPG_PRIVATE_KEY" >&2; exit 1; }
# Sign with rpm's DEFAULT __gpg_sign_cmd — it expands %{__signature_filename}/%{__plaintext_filename}
# correctly. (A custom __gpg_sign_cmd passed via --define reached gpg with those filename macros
# UNEXPANDED -> "No such file or directory".) Just point rpm at our key; the GNUPGHOME above
# (passphrase-less key + loopback) lets gpg sign headless.
for rpm in dist/*.rpm; do
rpmsign --define "_gpg_name $KEYID" --addsign "$rpm"
done
# Verify locally so a bad signature fails the build before publishing.
rpm --import <(gpg --export --armor "$KEYID")
rpmkeys --checksig dist/*.rpm
echo "signed + verified $(find dist -name '*.rpm' | wc -l) RPM(s) with key $KEYID"