build: Sign RPM packages (#8046)

This commit is contained in:
Ayaz Salikhov
2026-08-19 13:46:11 +00:00
committed by GitHub
parent 368ff1afce
commit 1be4868875
6 changed files with 94 additions and 8 deletions

View File

@@ -92,7 +92,7 @@
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-028ccea"
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-a6983f8"
}
],
@@ -102,7 +102,7 @@
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-028ccea"
"image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-a6983f8"
}
]
}

View File

@@ -49,3 +49,4 @@ jobs:
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}

View File

@@ -120,3 +120,4 @@ jobs:
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
remote_password: ${{ secrets.NEXUS_REMOTE_PASSWORD }}
signing_key: ${{ secrets.NEXUS_PACKAGES_PRIVATE_KEY }}

View File

@@ -29,6 +29,9 @@ on:
remote_password:
description: "The password or token for that Nexus account."
required: false
signing_key:
description: "Armoured PGP private key used to sign the RPMs. Required when publishing."
required: false
defaults:
run:
@@ -98,6 +101,14 @@ jobs:
PKG_CHANNEL: ${{ steps.release_info.outputs.channel }}
run: ./package/build_pkg.sh
# Before the upload, so the artifact and the published package are the
# same bytes. DEBs are not signed, so the key is never set on that job.
- name: Sign RPM
if: ${{ inputs.publish && matrix.distro == 'rhel' }}
env:
PKG_SIGNING_KEY: ${{ secrets.signing_key }}
run: ./package/sign_rpm.sh "${BUILD_DIR}"
- name: Upload package artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:

View File

@@ -9,6 +9,7 @@ a build configured with `-Dvalidator_keys=ON`.
```
package/
build_pkg.sh Staging and build script (called by the CMake `package` target and CI)
sign_rpm.sh Signs the built RPMs (called by CI when publishing)
publish_pkg.sh Uploads built packages to the XRPLF Nexus repositories (called by CI)
rpm/
xrpld.spec RPM spec
@@ -32,7 +33,7 @@ package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm).
| Package type | Image (`package_configs.<distro>[].image` in `linux.json`) | Tools required |
| ------------ | ---------------------------------------------------------- | --------------------------------------------------- |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild` |
| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha>` | `rpmbuild`, `rpmsign` |
| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha>` | `dpkg-buildpackage`, debhelper with compat level 13 |
To print the full packaging matrix (artifact names and images) for the current
@@ -152,11 +153,14 @@ any `XRPLF` repository, `on-pr.yml` never. Both authenticate with the
`NEXUS_REMOTE_USERNAME` / `NEXUS_REMOTE_PASSWORD` secrets already used for the
Conan remote.
Nexus owns the repository metadata; nothing here signs or indexes anything. Worth
knowing:
Nexus owns the repository metadata; nothing here indexes anything. Worth knowing:
- Each apt-hosted repository needs a distribution and a PGP signing keypair
configured in Nexus, which rejects one created without a keypair.
configured in Nexus, which rejects one created without a keypair. Nexus signs
the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus at all, so `sign_rpm.sh`
signs the RPMs before they are uploaded, and rpm clients verify with
`gpgcheck=1` rather than `repo_gpgcheck=1`.
- yum metadata is rebuilt asynchronously, so a successful publish is not
immediately installable.
- Each job uploads only what it built, and uploads are not transactional, so a
@@ -212,8 +216,12 @@ fail early.
Flags are for explicit invocation; environment variables are intended for
CMake/CI integration. The CI workflow and the CMake `package` target both invoke
`build_pkg.sh` with no flags; CMake supplies `SRC_DIR`, `BUILD_DIR`, and
`PKG_RELEASE` via env, while CI supplies `BUILD_DIR` and `PKG_RELEASE` via env
and lets the script use defaults for the rest.
`PKG_RELEASE` via env, while CI supplies `BUILD_DIR`, `PKG_RELEASE` and
`PKG_CHANNEL` via env and lets the script use defaults for the rest.
Signing is not part of this script. `sign_rpm.sh` does it in a separate CI step
that only runs when publishing, so a published RPM is always signed and a local
build never needs a key.
It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls
`stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files,

65
package/sign_rpm.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
# Sign the RPMs built by build_pkg.sh. Nexus cannot sign hosted yum metadata, so
# the packages carry the signature themselves and rpm clients verify them with
# gpgcheck=1.
#
# Usage: sign_rpm.sh [package-dir]
#
# package-dir searched recursively for *.rpm ('build' by default)
#
# PKG_SIGNING_KEY must hold an armoured PGP private key. It has no flag, to keep
# the key out of the process list.
#
# There is no DEB equivalent: apt trusts the repository metadata, which Nexus
# signs, rather than the packages themselves.
pkg_dir="${1:-build}"
mapfile -d '' rpms < <(find "${pkg_dir}" -type f -name '*.rpm' -print0)
# Signing nothing would otherwise look like a successful signing.
if [[ ${#rpms[@]} -eq 0 ]]; then
echo "sign_rpm.sh: no RPMs found in ${pkg_dir}." >&2
exit 1
fi
: "${PKG_SIGNING_KEY:?is required}"
# Global, and expanded by the trap when it fires: the keyring holds an
# unencrypted private key, so it must go even if signing fails.
signing_home="$(mktemp -d)"
trap 'rm -rf "${signing_home}"' EXIT
export GNUPGHOME="${signing_home}"
printf '%s' "${PKG_SIGNING_KEY}" | gpg --batch --quiet --import
# Exactly one secret key, so that picking the first below is not a guess between
# several.
secrets="$(gpg --list-secret-keys --with-colons | grep -c '^sec:' || true)"
if [[ "${secrets}" -ne 1 ]]; then
echo "sign_rpm.sh: PKG_SIGNING_KEY must hold exactly one secret key, found ${secrets}." >&2
exit 1
fi
key="$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr:/ { print $10; exit }')"
echo "Signing ${#rpms[@]} RPM(s) with ${key}."
# Loopback pinentry: the key is unattended, so there is no tty to prompt on.
rpmsign \
--define "_gpg_name ${key}" \
--define "_gpg_sign_cmd_extra_args --pinentry-mode loopback --batch --yes" \
--addsign "${rpms[@]}"
# rpmsign can exit 0 having attached nothing, and an unsigned package is only
# rejected later, on the installing machine. Both header tags are checked
# because an RSA signature lands in RSAHEADER and a DSA or EdDSA one in
# DSAHEADER.
for pkg in "${rpms[@]}"; do
signature="$(rpm --query --queryformat '%{RSAHEADER:pgpsig}%{DSAHEADER:pgpsig}' --package "${pkg}")"
if [[ "${signature}" == "(none)(none)" ]]; then
echo "sign_rpm.sh: ${pkg} is unsigned after rpmsign." >&2
exit 1
fi
done