From fa46641c7d612aeb920957c841565e26c5dff0dd Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Mon, 24 Aug 2026 18:24:17 -0400 Subject: [PATCH] Adding publish-rpm-repository action/workflow Signed-off-by: Alex Lovell-Troy --- .github/actionlint.yaml | 8 + .github/workflows/ci.yml | 12 + .github/workflows/publish-rpm-repository.yml | 150 +++++++++ README.md | 38 +++ actions/publish-rpm-repository/README.md | 142 ++++++++ actions/publish-rpm-repository/action.yml | 139 ++++++++ actions/publish-rpm-repository/publish.sh | 335 +++++++++++++++++++ tests/test-publish-rpm-repository.sh | 156 +++++++++ 8 files changed, 980 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/publish-rpm-repository.yml create mode 100644 actions/publish-rpm-repository/README.md create mode 100644 actions/publish-rpm-repository/action.yml create mode 100755 actions/publish-rpm-repository/publish.sh create mode 100755 tests/test-publish-rpm-repository.sh diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..b0a546d --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +paths: + .github/workflows/publish-rpm-repository.yml: + ignore: + - 'property "workflow_repository" is not defined in object type' + - 'property "workflow_sha" is not defined in object type' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4098a00..748692f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,3 +15,15 @@ jobs: - uses: actions/checkout@v6.0.3 - name: actionlint uses: raven-actions/actionlint@v2.1.2 + + test-rpm-repository-publisher: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.3 + - name: Exercise RPM repository publisher + run: | + docker run --rm \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace \ + rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \ + ./tests/test-publish-rpm-repository.sh diff --git a/.github/workflows/publish-rpm-repository.yml b/.github/workflows/publish-rpm-repository.yml new file mode 100644 index 0000000..7e55575 --- /dev/null +++ b/.github/workflows/publish-rpm-repository.yml @@ -0,0 +1,150 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: Publish RPM repository +run-name: Publish RPM repository for ${{ github.ref }} + +on: + workflow_call: + inputs: + artifact-name-signed-rpms: + description: 'Name of the signed RPM artifact to publish' + type: string + default: 'rpms-signed' + required: false + artifact-name-package-signing-keys: + description: 'Name of the certified ephemeral package public-key artifact' + type: string + default: 'public-keys' + required: false + repository-path: + description: 'Path below the bucket, for example stable/el9/x86_64' + type: string + required: true + s3-bucket: + description: 'Destination S3 bucket' + type: string + required: true + aws-region: + description: 'AWS region containing the destination bucket' + type: string + required: true + aws-role-arn: + description: 'GitHub OIDC publisher role ARN' + type: string + required: true + aws-account-id: + description: 'Expected AWS account ID for confused-deputy protection' + type: string + required: true + cloudfront-distribution-id: + description: 'CloudFront distribution to invalidate after publication' + type: string + required: false + public-base-url: + description: 'Public repository base URL, for example https://rpm.openchami.org' + type: string + required: false + metadata-signing-key-fingerprint: + description: 'Full fingerprint of the durable repository metadata signing key' + type: string + required: true + package-master-fingerprint: + description: 'Full fingerprint of the trusted package-signing master key' + type: string + required: true + repository-key-filename: + description: 'Published filename for the metadata signing public key' + type: string + default: 'RPM-GPG-KEY-OpenCHAMI' + required: false + deployment-environment: + description: 'GitHub Environment containing publication protections and key secrets' + type: string + default: 'rpm-publish' + required: false + dry-run: + description: 'Build and verify repository metadata without accessing AWS' + type: boolean + default: false + required: false + outputs: + repository-url: + description: 'Published repository URL' + value: ${{ jobs.publish.outputs.repository-url }} + package-count: + description: 'Number of packages in the generated repository' + value: ${{ jobs.publish.outputs.package-count }} + repomd-sha256: + description: 'SHA-256 digest of the committed repomd.xml' + value: ${{ jobs.publish.outputs.repomd-sha256 }} + invalidation-id: + description: 'CloudFront invalidation ID, when requested' + value: ${{ jobs.publish.outputs.invalidation-id }} + +permissions: + contents: read + id-token: write + +concurrency: + group: rpm-repository-${{ inputs.s3-bucket }}-${{ inputs.repository-path }} + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + environment: ${{ inputs.deployment-environment }} + outputs: + repository-url: ${{ steps.publish.outputs.repository-url }} + package-count: ${{ steps.publish.outputs.package-count }} + repomd-sha256: ${{ steps.publish.outputs.repomd-sha256 }} + invalidation-id: ${{ steps.publish.outputs.invalidation-id }} + steps: + - name: Checkout publisher action at the reusable workflow commit + uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 + with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: .openchami-actions + persist-credentials: false + + - name: Download signed RPM artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ${{ inputs.artifact-name-signed-rpms }} + path: dist/rpms + + - name: Download package signing keys + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ${{ inputs.artifact-name-package-signing-keys }} + path: dist/package-keys + + - name: Configure AWS credentials + if: ${{ !inputs.dry-run }} + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 + with: + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ inputs.aws-role-arn }} + role-session-name: OpenCHAMI-RPM-${{ github.run_id }} + allowed-account-ids: ${{ inputs.aws-account-id }} + unset-current-credentials: true + + - name: Publish repository + id: publish + uses: ./.openchami-actions/actions/publish-rpm-repository + with: + rpm-path: dist/rpms + repository-path: ${{ inputs.repository-path }} + s3-bucket: ${{ inputs.s3-bucket }} + cloudfront-distribution-id: ${{ inputs.cloudfront-distribution-id }} + public-base-url: ${{ inputs.public-base-url }} + package-signing-keys-path: dist/package-keys + package-master-public-key-asc: ${{ secrets.MASTER_PUBLIC_ASC }} + package-master-fingerprint: ${{ inputs.package-master-fingerprint }} + authorized-package-repo-fingerprints: ${{ vars.PACKAGE_REPO_CERT_FINGERPRINTS }} + metadata-signing-key-armored-b64: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_B64 }} + metadata-signing-key-fingerprint: ${{ inputs.metadata-signing-key-fingerprint }} + metadata-signing-key-passphrase: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_PASSPHRASE }} + repository-key-filename: ${{ inputs.repository-key-filename }} + dry-run: ${{ inputs.dry-run }} diff --git a/README.md b/README.md index 4b2bebf..a271cb7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Reusable GitHub Actions for CI/CD. - `actions/gpg-sign-rpm`: RPM signing with ephemeral keys - `actions/gpg-check-key-expiration`: Fails CI if a signing key is expired or expiring soon - `actions/gpg-verify-trust-chain`: Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs +- `actions/publish-rpm-repository`: Generates durable-key-signed repository metadata and safely publishes RPM repositories to S3 - `.github/workflows/go-build-release.yml`: Reusable workflow for GoReleaser builds - `.github/workflows/docker-build-release.yml`: Reusable workflow for multi-arch container image builds - `.github/workflows/build-publish-container-goreleaser.yml`: Builds and publishes a container image via GoReleaser @@ -21,6 +22,7 @@ Reusable GitHub Actions for CI/CD. - `.github/workflows/gpg-sign-artifacts.yml`: Signs unsigned RPM artifacts with a per-run ephemeral key - `.github/workflows/validate-rpm-quadlet.yml`: Validates a signed quadlet RPM's installed file list - `.github/workflows/release-signed-artifacts.yml`: Publishes a GitHub Release with signed RPMs and public keys +- `.github/workflows/publish-rpm-repository.yml`: Publishes aggregated RPMs through S3 and CloudFront using GitHub OIDC - `.github/workflows/lint-workflows.yml`: Reusable workflow that lints workflow files (actionlint + zizmor) - `.github/workflows/govulncheck.yml`: Reusable workflow that scans Go modules for known CVEs - `.github/workflows/dependency-review.yml`: Reusable workflow that gates PRs introducing CVE-flagged deps @@ -211,6 +213,36 @@ jobs: uses: OpenCHAMI/github-actions/.github/workflows/release-signed-artifacts.yml@v3.5 ``` +### publish-rpm-repository (Reusable Workflow) + +Aggregates signed RPM artifacts into a DNF/YUM repository, signs `repomd.xml` +with a durable repository metadata key, publishes to S3 using GitHub OIDC, and +invalidates only mutable CloudFront metadata paths. The calling repository's +`rpm-publish` GitHub Environment supplies the durable signing-key secrets and +can enforce required reviewers. + +```yaml +jobs: + publish: + uses: OpenCHAMI/github-actions/.github/workflows/publish-rpm-repository.yml@v3.6 + with: + repository-path: stable/el9/x86_64 + s3-bucket: ${{ vars.RPM_REPOSITORY_BUCKET }} + aws-region: us-east-1 + aws-role-arn: ${{ vars.RPM_REPOSITORY_PUBLISHER_ROLE_ARN }} + aws-account-id: ${{ vars.AWS_ACCOUNT_ID }} + cloudfront-distribution-id: ${{ vars.RPM_REPOSITORY_DISTRIBUTION_ID }} + public-base-url: https://rpm.openchami.org + package-master-fingerprint: ${{ vars.MASTER_FPR }} + metadata-signing-key-fingerprint: ${{ vars.RPM_REPOSITORY_SIGNING_KEY_FINGERPRINT }} +``` + +The service repositories continue signing RPM packages with certified ephemeral +keys. Publication verifies those chains and emits a rotating DNF package-key +bundle. Only repository metadata uses the durable key. See the +[action documentation](actions/publish-rpm-repository/README.md) for publication +ordering, key handling, and client trust requirements. + ## Actions ### gpg-ephemeral-key (Deprecated - use gpg-configure-release-keys) @@ -228,6 +260,12 @@ Fails CI if the provided signing key is expired or expiring within a threshold. ### gpg-verify-trust-chain Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs. See the [action README](actions/gpg-verify-trust-chain/README.md). +### publish-rpm-repository + +Builds repository metadata from signed RPMs, signs `repomd.xml` with the durable +repository metadata key, and publishes immutable packages before the metadata +commit point. See the [action README](actions/publish-rpm-repository/README.md). + ## Security Model Trust chain: `Ephemeral Key <- Repo Cert Key <- Offline Master Key`. diff --git a/actions/publish-rpm-repository/README.md b/actions/publish-rpm-repository/README.md new file mode 100644 index 0000000..aa72be6 --- /dev/null +++ b/actions/publish-rpm-repository/README.md @@ -0,0 +1,142 @@ + + +# Publish RPM Repository Action + +Builds DNF/YUM repository metadata for signed RPMs, signs `repomd.xml` with a +durable repository metadata key, and safely publishes the repository to S3. + +Each service repository signs its RPMs with a certified per-run ephemeral key +before the release workflow aggregates them. This action authenticates every +ephemeral key through the trusted master and repository certification-key chain, +then verifies every incoming and previously published RPM before signing the +repository metadata. + +## Publication model + +The action restores previously published immutable RPMs, adds the incoming RPMs, +and regenerates metadata for the complete package set. It never deletes packages +or metadata from S3. Publication occurs in this order: + +1. Immutable RPMs under `Packages/`. +2. Content-addressed files under `repodata/`. +3. Immutable authenticated package-key archives. +4. Rotating package-key bundle and durable metadata signing public key. +5. `repodata/repomd.xml.asc`. +6. `repodata/repomd.xml` as the final metadata pointer. +7. A CloudFront invalidation limited to metadata and rotating public keys. + +S3 cannot atomically replace `repomd.xml` and its detached signature. Publishing +the signature first makes the short update interval fail closed: a client may +temporarily reject mismatched metadata and retry, but it cannot accept unsigned +or partially published metadata. + +The reusable workflow serializes publishers for a repository path. Production +publication should only be invoked by the central `OpenCHAMI/release` repository +so publishers in separate repositories cannot race. + +## Inputs + +| Input | Required | Default | Description | +|---|---:|---|---| +| `rpm-path` | Yes | | File or directory containing signed RPMs | +| `repository-path` | Yes | | Bucket prefix such as `stable/el9/x86_64` | +| `s3-bucket` | For publication | | Destination bucket | +| `cloudfront-distribution-id` | No | | Distribution to invalidate | +| `public-base-url` | No | | Public base URL such as `https://rpm.openchami.org` | +| `package-signing-keys-path` | Yes | | Directory containing paired `repo-cert.pub.asc` and `ephemeral.pub.asc` files | +| `package-master-public-key-asc` | Yes | | Trusted package-signing master public key | +| `package-master-fingerprint` | Yes | | Full trusted package-signing master fingerprint | +| `authorized-package-repo-fingerprints` | Yes | | Current repo-cert fingerprints allowed to certify new ephemeral keys | +| `package-key-bundle-filename` | No | `RPM-GPG-KEY-OpenCHAMI-Packages` | Rotating authenticated package-key bundle | +| `metadata-signing-key-armored-b64` | Yes | | Base64-encoded armored durable secret key | +| `metadata-signing-key-fingerprint` | Yes | | Full durable signing-key fingerprint | +| `metadata-signing-key-passphrase` | No | | Secret-key passphrase | +| `repository-key-filename` | No | `RPM-GPG-KEY-OpenCHAMI` | Public key filename | +| `require-signed-rpms` | No | `true` | Reject RPMs without package signatures | +| `dry-run` | No | `false` | Build and verify locally without AWS | + +## Outputs + +| Output | Description | +|---|---| +| `repository-directory` | Local complete repository directory | +| `repository-url` | Public URL, S3 URL, or dry-run file URL | +| `package-count` | Number of RPMs in the generated repository | +| `repomd-sha256` | SHA-256 digest of `repomd.xml` | +| `invalidation-id` | CloudFront invalidation ID when requested | + +## Direct usage + +The caller must configure short-lived AWS credentials before invoking the action. + +```yaml +permissions: + contents: read + id-token: write + +steps: + - uses: aws-actions/configure-aws-credentials@v6.2.3 + with: + aws-region: us-east-1 + role-to-assume: ${{ vars.RPM_REPOSITORY_PUBLISHER_ROLE_ARN }} + allowed-account-ids: ${{ vars.AWS_ACCOUNT_ID }} + + - uses: OpenCHAMI/github-actions/actions/publish-rpm-repository@v3.6 + with: + rpm-path: dist/rpms + repository-path: stable/el9/x86_64 + s3-bucket: ${{ vars.RPM_REPOSITORY_BUCKET }} + cloudfront-distribution-id: ${{ vars.RPM_REPOSITORY_DISTRIBUTION_ID }} + public-base-url: https://rpm.openchami.org + package-signing-keys-path: dist/package-keys + package-master-public-key-asc: ${{ secrets.MASTER_PUBLIC_ASC }} + package-master-fingerprint: ${{ vars.MASTER_FPR }} + authorized-package-repo-fingerprints: ${{ vars.PACKAGE_REPO_CERT_FINGERPRINTS }} + metadata-signing-key-armored-b64: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_B64 }} + metadata-signing-key-fingerprint: ${{ vars.RPM_REPOSITORY_SIGNING_KEY_FINGERPRINT }} + metadata-signing-key-passphrase: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_PASSPHRASE }} +``` + +For production, prefer the repository's `publish-rpm-repository.yml` reusable +workflow, which supplies OIDC permissions, GitHub Environment protections, and +concurrency controls. + +## Durable key handling + +Store these secrets in the protected `rpm-publish` GitHub Environment of the +calling repository: + +- `RPM_REPOSITORY_SIGNING_KEY_B64` +- `RPM_REPOSITORY_SIGNING_KEY_PASSPHRASE` +- `MASTER_PUBLIC_ASC` + +Store the non-secret fingerprint and AWS identifiers as environment variables. +The action imports the durable secret key into an isolated temporary +`GNUPGHOME`, exports only its public key into the repository, and shreds the +temporary keyring at exit. + +`PACKAGE_REPO_CERT_FINGERPRINTS` is a comma, whitespace, or newline-separated +allowlist maintained in the protected environment. Historical repo-cert keys +remain archived so old RPM signatures can be verified, but only keys in this +current allowlist may certify newly submitted ephemeral package keys. Remove a +retired or compromised repo-cert fingerprint before the next publication. + +Clients should configure both `gpgcheck=1` for ephemeral package signatures and +`repo_gpgcheck=1` for the durable `repomd.xml` signature. The `.repo` file must +list both published keys: + +```ini +gpgkey=https://rpm.openchami.org/stable/el9/x86_64/RPM-GPG-KEY-OpenCHAMI-Packages + https://rpm.openchami.org/stable/el9/x86_64/RPM-GPG-KEY-OpenCHAMI +``` + +The package bundle contains only ephemeral public keys whose certification +chains were verified during publication. DNF imports those concrete keys rather +than being expected to discover the OpenPGP certification chain itself. + +## License + +MIT diff --git a/actions/publish-rpm-repository/action.yml b/actions/publish-rpm-repository/action.yml new file mode 100644 index 0000000..4df5391 --- /dev/null +++ b/actions/publish-rpm-repository/action.yml @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: 'Publish RPM repository' +author: 'OpenCHAMI' +description: 'Builds signed RPM repository metadata and publishes it to S3 in safe commit order' +branding: + icon: 'upload-cloud' + color: 'orange' + +inputs: + rpm-path: + description: 'File or directory containing signed RPMs to publish' + required: true + repository-path: + description: 'Path below the bucket, for example stable/el9/x86_64' + required: true + s3-bucket: + description: 'Destination S3 bucket; required unless dry-run is true' + required: false + cloudfront-distribution-id: + description: 'CloudFront distribution to invalidate after publication' + required: false + public-base-url: + description: 'Public repository base URL, for example https://rpm.openchami.org' + required: false + package-signing-keys-path: + description: 'Directory containing repo-cert.pub.asc and ephemeral.pub.asc package-key pairs' + required: true + package-master-public-key-asc: + description: 'Trusted ASCII-armored master public key that certifies package repository keys' + required: true + package-master-fingerprint: + description: 'Full fingerprint of the trusted package-signing master key' + required: true + authorized-package-repo-fingerprints: + description: 'Comma, space, or newline-separated current repo-cert fingerprints allowed to certify new package keys' + required: true + package-key-bundle-filename: + description: 'Published filename containing authenticated ephemeral RPM public keys' + default: 'RPM-GPG-KEY-OpenCHAMI-Packages' + required: false + metadata-signing-key-armored-b64: + description: 'Base64-encoded ASCII-armored durable metadata signing secret key' + required: true + metadata-signing-key-fingerprint: + description: 'Full fingerprint of the durable metadata signing key' + required: true + metadata-signing-key-passphrase: + description: 'Passphrase for the metadata signing key, if protected' + required: false + repository-key-filename: + description: 'Published filename for the metadata signing public key' + default: 'RPM-GPG-KEY-OpenCHAMI' + required: false + require-signed-rpms: + description: 'Reject RPMs that do not contain a package signature' + default: 'true' + required: false + dry-run: + description: 'Build and verify locally without reading from or writing to AWS' + default: 'false' + required: false + +outputs: + repository-directory: + description: 'Local directory containing the complete generated repository' + value: ${{ steps.publish.outputs.repository-directory }} + repository-url: + description: 'Public URL when public-base-url is set, otherwise the S3 URL' + value: ${{ steps.publish.outputs.repository-url }} + package-count: + description: 'Number of RPMs represented by the generated repository' + value: ${{ steps.publish.outputs.package-count }} + repomd-sha256: + description: 'SHA-256 digest of the committed repomd.xml' + value: ${{ steps.publish.outputs.repomd-sha256 }} + invalidation-id: + description: 'CloudFront invalidation ID, when an invalidation was requested' + value: ${{ steps.publish.outputs.invalidation-id }} + +runs: + using: composite + steps: + - name: Install repository tooling + shell: bash + env: + DRY_RUN: ${{ inputs.dry-run }} + run: | + set -euo pipefail + packages=(createrepo-c rpm gnupg) + if [[ "$DRY_RUN" != 'true' ]] && ! command -v aws >/dev/null 2>&1; then + packages+=(awscli) + fi + + missing=false + for command in createrepo_c rpm gpg; do + command -v "$command" >/dev/null 2>&1 || missing=true + done + if [[ "$DRY_RUN" != 'true' ]]; then + command -v aws >/dev/null 2>&1 || missing=true + fi + [[ "$missing" == 'false' ]] && exit 0 + + sudo_cmd=() + command -v sudo >/dev/null 2>&1 && sudo_cmd=(sudo) + if command -v apt-get >/dev/null 2>&1; then + "${sudo_cmd[@]}" apt-get update -qq + "${sudo_cmd[@]}" apt-get install -y -qq --no-install-recommends "${packages[@]}" + elif command -v dnf >/dev/null 2>&1; then + dnf_packages=(createrepo_c rpm gnupg2) + [[ "$DRY_RUN" == 'true' ]] || dnf_packages+=(awscli2) + "${sudo_cmd[@]}" dnf install -y -q "${dnf_packages[@]}" + else + echo '::error::Unsupported runner: apt-get or dnf is required' + exit 1 + fi + + - name: Build, sign, and publish repository + id: publish + shell: bash + env: + INPUT_RPM_PATH: ${{ inputs.rpm-path }} + INPUT_REPOSITORY_PATH: ${{ inputs.repository-path }} + INPUT_S3_BUCKET: ${{ inputs.s3-bucket }} + INPUT_CLOUDFRONT_DISTRIBUTION_ID: ${{ inputs.cloudfront-distribution-id }} + INPUT_PUBLIC_BASE_URL: ${{ inputs.public-base-url }} + INPUT_PACKAGE_SIGNING_KEYS_PATH: ${{ inputs.package-signing-keys-path }} + INPUT_PACKAGE_MASTER_PUBLIC_KEY_ASC: ${{ inputs.package-master-public-key-asc }} + INPUT_PACKAGE_MASTER_FINGERPRINT: ${{ inputs.package-master-fingerprint }} + INPUT_AUTHORIZED_PACKAGE_REPO_FINGERPRINTS: ${{ inputs.authorized-package-repo-fingerprints }} + INPUT_PACKAGE_KEY_BUNDLE_FILENAME: ${{ inputs.package-key-bundle-filename }} + INPUT_METADATA_SIGNING_KEY_ARMORED_B64: ${{ inputs.metadata-signing-key-armored-b64 }} + INPUT_METADATA_SIGNING_KEY_FINGERPRINT: ${{ inputs.metadata-signing-key-fingerprint }} + INPUT_METADATA_SIGNING_KEY_PASSPHRASE: ${{ inputs.metadata-signing-key-passphrase }} + INPUT_REPOSITORY_KEY_FILENAME: ${{ inputs.repository-key-filename }} + INPUT_REQUIRE_SIGNED_RPMS: ${{ inputs.require-signed-rpms }} + INPUT_DRY_RUN: ${{ inputs.dry-run }} + run: '"${{ github.action_path }}/publish.sh"' diff --git a/actions/publish-rpm-repository/publish.sh b/actions/publish-rpm-repository/publish.sh new file mode 100755 index 0000000..fba126b --- /dev/null +++ b/actions/publish-rpm-repository/publish.sh @@ -0,0 +1,335 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +set -euo pipefail + +fail() { + echo "::error::$*" >&2 + exit 1 +} + +normalize_bool() { + case "${1,,}" in + true|false) printf '%s' "${1,,}" ;; + *) fail "expected true or false, got: $1" ;; + esac +} + +rpm_path=${INPUT_RPM_PATH:?rpm-path is required} +repository_path=${INPUT_REPOSITORY_PATH:?repository-path is required} +s3_bucket=${INPUT_S3_BUCKET:-} +distribution_id=${INPUT_CLOUDFRONT_DISTRIBUTION_ID:-} +public_base_url=${INPUT_PUBLIC_BASE_URL:-} +package_keys_path=${INPUT_PACKAGE_SIGNING_KEYS_PATH:?package-signing-keys-path is required} +package_master_public_key=${INPUT_PACKAGE_MASTER_PUBLIC_KEY_ASC:?package master public key is required} +package_master_fingerprint=${INPUT_PACKAGE_MASTER_FINGERPRINT:?package master fingerprint is required} +authorized_package_repo_fingerprints=${INPUT_AUTHORIZED_PACKAGE_REPO_FINGERPRINTS:?authorized package repo fingerprints are required} +package_key_bundle_filename=${INPUT_PACKAGE_KEY_BUNDLE_FILENAME:-RPM-GPG-KEY-OpenCHAMI-Packages} +signing_key_b64=${INPUT_METADATA_SIGNING_KEY_ARMORED_B64:?metadata signing key is required} +signing_fingerprint=${INPUT_METADATA_SIGNING_KEY_FINGERPRINT:?metadata signing fingerprint is required} +signing_passphrase=${INPUT_METADATA_SIGNING_KEY_PASSPHRASE:-} +key_filename=${INPUT_REPOSITORY_KEY_FILENAME:-RPM-GPG-KEY-OpenCHAMI} +require_signed_rpms=$(normalize_bool "${INPUT_REQUIRE_SIGNED_RPMS:-true}") +dry_run=$(normalize_bool "${INPUT_DRY_RUN:-false}") + +repository_path=${repository_path#/} +repository_path=${repository_path%/} +[[ -n "$repository_path" ]] || fail 'repository-path cannot be empty' +[[ "$repository_path" != *'..'* ]] || fail 'repository-path cannot contain ..' +[[ "$repository_path" =~ ^[A-Za-z0-9._/-]+$ ]] || fail 'repository-path contains unsupported characters' +[[ "$key_filename" =~ ^[A-Za-z0-9._-]+$ ]] || fail 'repository-key-filename contains unsupported characters' +[[ "$package_key_bundle_filename" =~ ^[A-Za-z0-9._-]+$ ]] || fail 'package-key-bundle-filename contains unsupported characters' +[[ "$package_master_fingerprint" =~ ^[A-Fa-f0-9]{40,64}$ ]] || fail 'package master fingerprint must be a full fingerprint' +[[ "$signing_fingerprint" =~ ^[A-Fa-f0-9]{40,64}$ ]] || fail 'metadata signing fingerprint must be a full fingerprint' +[[ -d "$package_keys_path" ]] || fail "package-signing-keys-path does not exist: $package_keys_path" +if [[ "$dry_run" == 'false' ]]; then + [[ -n "$s3_bucket" ]] || fail 's3-bucket is required unless dry-run is true' + [[ "$s3_bucket" =~ ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ ]] || fail 'invalid S3 bucket name' + command -v aws >/dev/null 2>&1 || fail 'aws CLI is required for publication' +fi + +for command in createrepo_c rpm gpg base64 sha256sum; do + command -v "$command" >/dev/null 2>&1 || fail "$command is required" +done + +work_root=$(mktemp -d "${RUNNER_TEMP:-/tmp}/openchami-rpm-repo.XXXXXX") +repository_dir="$work_root/repository" +packages_dir="$repository_dir/Packages" +package_keys_dir="$repository_dir/keys/packages" +repo_keys_dir="$package_keys_dir/repository" +ephemeral_keys_dir="$package_keys_dir/ephemeral" +package_key_bundle="$repository_dir/$package_key_bundle_filename" +gnupg_home="$work_root/gnupg" +package_trust_gnupg="$work_root/package-trust-gnupg" +package_rpm_db="$work_root/package-rpmdb" +package_master_key_file="$work_root/package-master.pub.asc" +signing_key_file="$work_root/metadata-signing-key.asc" +mkdir -p "$packages_dir" "$repo_keys_dir" "$ephemeral_keys_dir" \ + "$gnupg_home" "$package_trust_gnupg" "$package_rpm_db" +chmod 700 "$gnupg_home" "$package_trust_gnupg" "$package_rpm_db" + +cleanup() { + if [[ -f "$signing_key_file" ]]; then + shred -u "$signing_key_file" 2>/dev/null || rm -f "$signing_key_file" + fi + if [[ -d "$gnupg_home" ]]; then + find "$gnupg_home" -type f -exec shred -u {} + 2>/dev/null || true + rm -rf "$gnupg_home" + fi + rm -rf "$package_trust_gnupg" "$package_rpm_db" + rm -f "$package_master_key_file" +} +trap cleanup EXIT + +export GNUPGHOME=$gnupg_home + +if [[ "$dry_run" == 'false' ]]; then + echo 'Restoring existing immutable packages from S3...' + aws s3 sync "s3://$s3_bucket/$repository_path/Packages/" "$packages_dir/" \ + --exclude '*' --include '*.rpm' --only-show-errors + aws s3 sync "s3://$s3_bucket/$repository_path/keys/packages/" "$package_keys_dir/" \ + --exclude '*' --include '*.asc' --only-show-errors +fi + +mapfile -d '' incoming_rpms < <( + if [[ -f "$rpm_path" ]]; then + [[ "$rpm_path" == *.rpm ]] && printf '%s\0' "$rpm_path" + elif [[ -d "$rpm_path" ]]; then + find "$rpm_path" -type f -name '*.rpm' -print0 + else + fail "rpm-path does not exist: $rpm_path" + fi +) +(( ${#incoming_rpms[@]} > 0 )) || fail "no RPMs found at $rpm_path" + +for rpm_file in "${incoming_rpms[@]}"; do + destination="$packages_dir/$(basename "$rpm_file")" + if [[ -e "$destination" ]]; then + cmp -s "$rpm_file" "$destination" || fail "immutable RPM collision: $(basename "$rpm_file")" + continue + fi + cp "$rpm_file" "$destination" +done + +public_key_fingerprint() { + local key_file=$1 + local fingerprints + fingerprints=$(gpg --batch --with-colons --import-options show-only --import "$key_file" 2>/dev/null | + awk -F: '$1 == "pub" {want=1; count++} want && $1 == "fpr" {print $10; want=0} END {if (count != 1) exit 1}') || + fail "expected exactly one public key in $key_file" + printf '%s' "$fingerprints" +} + +archive_public_key() { + local source_file=$1 + local destination_file=$2 + if [[ -e "$destination_file" ]]; then + cmp -s "$source_file" "$destination_file" || fail "public-key collision: $(basename "$destination_file")" + return + fi + cp "$source_file" "$destination_file" +} + +check_certification() { + local signer_id=${1: -16} + local subject_fingerprint=$2 + gpg --batch --check-sigs --with-colons "$subject_fingerprint" 2>/dev/null | + awk -F: -v signer_id="${signer_id^^}" \ + '$1 == "sig" && $2 == "!" && toupper($5) == signer_id {found=1} END {exit !found}' +} + +printf '%s\n' "$package_master_public_key" > "$package_master_key_file" +actual_package_master_fingerprint=$(public_key_fingerprint "$package_master_key_file") +[[ "${actual_package_master_fingerprint^^}" == "${package_master_fingerprint^^}" ]] || + fail 'package master public key does not match package-master-fingerprint' + +declare -A authorized_repo_fingerprints=() +while IFS= read -r authorized_fingerprint; do + [[ -n "$authorized_fingerprint" ]] || continue + [[ "$authorized_fingerprint" =~ ^[A-Fa-f0-9]{40,64}$ ]] || + fail "invalid authorized package repository fingerprint: $authorized_fingerprint" + authorized_repo_fingerprints["${authorized_fingerprint^^}"]=1 +done < <(tr ',[:space:]' '\n' <<<"$authorized_package_repo_fingerprints") +(( ${#authorized_repo_fingerprints[@]} > 0 )) || fail 'authorized package repository fingerprint allowlist is empty' + +mapfile -d '' incoming_ephemeral_keys < <(find "$package_keys_path" -type f -name 'ephemeral.pub.asc' -print0) +(( ${#incoming_ephemeral_keys[@]} > 0 )) || fail 'no ephemeral.pub.asc package signing keys found' +for ephemeral_key in "${incoming_ephemeral_keys[@]}"; do + repo_key="$(dirname "$ephemeral_key")/repo-cert.pub.asc" + [[ -f "$repo_key" ]] || fail "repo-cert.pub.asc not found beside $ephemeral_key" + repo_fingerprint=$(public_key_fingerprint "$repo_key") + ephemeral_fingerprint=$(public_key_fingerprint "$ephemeral_key") + [[ -n "${authorized_repo_fingerprints[${repo_fingerprint^^}]:-}" ]] || + fail "package repository key is not authorized for new ephemeral keys: $repo_fingerprint" + + pair_gnupg=$(mktemp -d "$work_root/package-key-pair.XXXXXX") + chmod 700 "$pair_gnupg" + GNUPGHOME="$pair_gnupg" gpg --batch --import \ + "$package_master_key_file" "$repo_key" "$ephemeral_key" >/dev/null 2>&1 + GNUPGHOME="$pair_gnupg" check_certification "$package_master_fingerprint" "$repo_fingerprint" || + fail "incoming package repository key is not certified by the trusted master: $repo_fingerprint" + GNUPGHOME="$pair_gnupg" check_certification "$repo_fingerprint" "$ephemeral_fingerprint" || + fail "incoming ephemeral package key is not certified by its authorized repository key: $ephemeral_fingerprint" + rm -rf "$pair_gnupg" + + archive_public_key "$repo_key" "$repo_keys_dir/${repo_fingerprint^^}.asc" + archive_public_key "$ephemeral_key" "$ephemeral_keys_dir/${ephemeral_fingerprint^^}.asc" +done + +export GNUPGHOME=$package_trust_gnupg +gpg --batch --import "$package_master_key_file" >/dev/null 2>&1 + +mapfile -d '' repo_key_files < <(find "$repo_keys_dir" -type f -name '*.asc' -print0) +mapfile -d '' ephemeral_key_files < <(find "$ephemeral_keys_dir" -type f -name '*.asc' -print0) +(( ${#repo_key_files[@]} > 0 )) || fail 'no authenticated package repository keys available' +(( ${#ephemeral_key_files[@]} > 0 )) || fail 'no authenticated ephemeral package keys available' + +repo_fingerprints=() +for repo_key_file in "${repo_key_files[@]}"; do + repo_fingerprints+=("$(public_key_fingerprint "$repo_key_file")") + gpg --batch --import "$repo_key_file" >/dev/null 2>&1 +done + +ephemeral_fingerprints=() +for ephemeral_key_file in "${ephemeral_key_files[@]}"; do + ephemeral_fingerprints+=("$(public_key_fingerprint "$ephemeral_key_file")") + gpg --batch --import "$ephemeral_key_file" >/dev/null 2>&1 +done + +for repo_fingerprint in "${repo_fingerprints[@]}"; do + check_certification "$package_master_fingerprint" "$repo_fingerprint" || + fail "package repository key is not certified by the trusted master: $repo_fingerprint" +done + +for ephemeral_fingerprint in "${ephemeral_fingerprints[@]}"; do + certified=false + for repo_fingerprint in "${repo_fingerprints[@]}"; do + if check_certification "$repo_fingerprint" "$ephemeral_fingerprint"; then + certified=true + break + fi + done + [[ "$certified" == 'true' ]] || fail "ephemeral package key is not certified by a trusted repository key: $ephemeral_fingerprint" +done + +gpg --batch --armor --export "${ephemeral_fingerprints[@]}" > "$package_key_bundle" +[[ -s "$package_key_bundle" ]] || fail 'package signing public-key bundle was empty' + +if [[ "$require_signed_rpms" == 'true' ]]; then + rpm --dbpath "$package_rpm_db" --initdb + rpm --dbpath "$package_rpm_db" --import "$package_key_bundle" + while IFS= read -r -d '' rpm_file; do + signature_result=$(rpm --dbpath "$package_rpm_db" -K "$rpm_file" 2>&1) || + fail "RPM signature verification failed: $rpm_file: $signature_result" + grep -Eqi 'digests signatures OK|signatures OK' <<<"$signature_result" || + fail "RPM signature verification was not trusted: $rpm_file: $signature_result" + done < <(find "$packages_dir" -type f -name '*.rpm' -print0) +fi + +export GNUPGHOME=$gnupg_home + +printf '%s' "$signing_key_b64" | base64 --decode > "$signing_key_file" +gpg --batch --import "$signing_key_file" +shred -u "$signing_key_file" 2>/dev/null || rm -f "$signing_key_file" + +gpg --batch --with-colons --list-secret-keys "$signing_fingerprint" | + awk -F: -v expected="${signing_fingerprint^^}" \ + '/^fpr:/ && toupper($10) == expected {found=1} END {exit !found}' || + fail 'requested metadata signing secret key was not imported' + +gpg --batch --armor --export "$signing_fingerprint" > "$repository_dir/$key_filename" +[[ -s "$repository_dir/$key_filename" ]] || fail 'metadata public-key export was empty' + +createrepo_c --update "$repository_dir" +repomd="$repository_dir/repodata/repomd.xml" +[[ -s "$repomd" ]] || fail 'createrepo_c did not produce repomd.xml' +rm -f "$repomd.asc" + +gpg_args=( + --batch + --yes + --armor + --detach-sign + --local-user "${signing_fingerprint}!" + --output "$repomd.asc" +) +if [[ -n "$signing_passphrase" ]]; then + printf '%s' "$signing_passphrase" | + gpg "${gpg_args[@]}" --pinentry-mode loopback --passphrase-fd 0 "$repomd" +else + gpg "${gpg_args[@]}" "$repomd" +fi +gpg --batch --verify "$repomd.asc" "$repomd" + +package_count=$(find "$packages_dir" -type f -name '*.rpm' | wc -l | tr -d ' ') +repomd_sha256=$(sha256sum "$repomd" | awk '{print $1}') +invalidation_id='' + +if [[ "$dry_run" == 'false' ]]; then + destination="s3://$s3_bucket/$repository_path" + echo 'Uploading immutable RPMs...' + aws s3 sync "$packages_dir/" "$destination/Packages/" \ + --exclude '*' --include '*.rpm' \ + --cache-control 'public,max-age=31536000,immutable' \ + --only-show-errors + + aws s3 sync "$package_keys_dir/" "$destination/keys/packages/" \ + --exclude '*' --include '*.asc' \ + --cache-control 'public,max-age=31536000,immutable' \ + --only-show-errors + + echo 'Uploading content-addressed repository metadata...' + while IFS= read -r -d '' metadata_file; do + relative_path=${metadata_file#"$repository_dir/"} + aws s3 cp "$metadata_file" "$destination/$relative_path" \ + --cache-control 'public,max-age=300,must-revalidate' \ + --only-show-errors + done < <(find "$repository_dir/repodata" -type f \ + ! -name 'repomd.xml' ! -name 'repomd.xml.asc' -print0) + + aws s3 cp "$repository_dir/$key_filename" "$destination/$key_filename" \ + --cache-control 'public,max-age=300,must-revalidate' \ + --content-type 'application/pgp-keys' \ + --only-show-errors + aws s3 cp "$package_key_bundle" "$destination/$package_key_bundle_filename" \ + --cache-control 'public,max-age=300,must-revalidate' \ + --content-type 'application/pgp-keys' \ + --only-show-errors + aws s3 cp "$repomd.asc" "$destination/repodata/repomd.xml.asc" \ + --cache-control 'public,max-age=300,must-revalidate' \ + --content-type 'application/pgp-signature' \ + --only-show-errors + + echo 'Publishing repomd.xml commit point...' + aws s3 cp "$repomd" "$destination/repodata/repomd.xml" \ + --cache-control 'public,max-age=300,must-revalidate' \ + --content-type 'application/xml' \ + --only-show-errors + + if [[ -n "$distribution_id" ]]; then + invalidation_id=$(aws cloudfront create-invalidation \ + --distribution-id "$distribution_id" \ + --paths "/$repository_path/repodata/*" "/$repository_path/$key_filename" \ + "/$repository_path/$package_key_bundle_filename" \ + --query 'Invalidation.Id' --output text) + fi +fi + +if [[ -n "$public_base_url" ]]; then + repository_url="${public_base_url%/}/$repository_path" +elif [[ -n "$s3_bucket" ]]; then + repository_url="s3://$s3_bucket/$repository_path" +else + repository_url="file://$repository_dir" +fi + +{ + echo "repository-directory=$repository_dir" + echo "repository-url=$repository_url" + echo "package-count=$package_count" + echo "repomd-sha256=$repomd_sha256" + echo "invalidation-id=$invalidation_id" +} >> "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" + +echo "Generated repository with $package_count package(s): $repository_url" diff --git a/tests/test-publish-rpm-repository.sh b/tests/test-publish-rpm-repository.sh new file mode 100755 index 0000000..0b0e40f --- /dev/null +++ b/tests/test-publish-rpm-repository.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +set -euo pipefail + +dnf install -y -q createrepo_c rpm-build rpm-sign gnupg2 + +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT +export HOME="$work_dir/home" +mkdir -p "$HOME" "$HOME/rpmbuild"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + +package_gnupg="$work_dir/package-gnupg" +metadata_gnupg="$work_dir/metadata-gnupg" +mkdir -m 700 "$package_gnupg" "$metadata_gnupg" + +fingerprint_for_uid() { + GNUPGHOME="$package_gnupg" gpg --batch --with-colons --list-secret-keys | + awk -F: -v marker="$1" '/^fpr:/ {fpr=$10} /^uid:/ && index($10, marker) {print fpr; exit}' +} + +GNUPGHOME="$package_gnupg" gpg --batch --passphrase '' --quick-generate-key \ + 'OpenCHAMI Test Master ' rsa2048 cert 0 +package_master_fingerprint=$(fingerprint_for_uid 'test-master@openchami.org') +GNUPGHOME="$package_gnupg" gpg --batch --passphrase '' --quick-generate-key \ + 'OpenCHAMI Test Repository ' rsa2048 cert 0 +package_repo_fingerprint=$(fingerprint_for_uid 'test-repo@openchami.org') +GNUPGHOME="$package_gnupg" gpg --batch --yes --quick-sign-key \ + --local-user "$package_master_fingerprint" "$package_repo_fingerprint" +GNUPGHOME="$package_gnupg" gpg --batch --passphrase '' --quick-generate-key \ + 'OpenCHAMI Test Package ' rsa2048 sign 1d +package_fingerprint=$(fingerprint_for_uid 'test-package@openchami.org') +GNUPGHOME="$package_gnupg" gpg --batch --yes --quick-sign-key \ + --local-user "$package_repo_fingerprint" "$package_fingerprint" + +package_keys_dir="$work_dir/package-keys/test-package" +mkdir -p "$package_keys_dir" +GNUPGHOME="$package_gnupg" gpg --batch --armor \ + --export "${package_repo_fingerprint}!" > "$package_keys_dir/repo-cert.pub.asc" +GNUPGHOME="$package_gnupg" gpg --batch --armor \ + --export "${package_fingerprint}!" > "$package_keys_dir/ephemeral.pub.asc" +package_master_public_key=$(GNUPGHOME="$package_gnupg" gpg --batch --armor \ + --export "${package_master_fingerprint}!") + +cat > "$HOME/rpmbuild/SPECS/openchami-publisher-test.spec" <<'SPEC' +Name: openchami-publisher-test +Version: 1.0.0 +Release: 1%{?dist} +Summary: Test package for the RPM repository publisher +License: MIT +BuildArch: noarch + +%description +Test package for the OpenCHAMI RPM repository publisher action. + +%install +mkdir -p %{buildroot}/usr/share/openchami-publisher-test +printf 'publisher test\n' > %{buildroot}/usr/share/openchami-publisher-test/result + +%files +/usr/share/openchami-publisher-test/result +SPEC + +rpmbuild -bb "$HOME/rpmbuild/SPECS/openchami-publisher-test.spec" +test_rpm=$(find "$HOME/rpmbuild/RPMS" -type f -name '*.rpm' -print -quit) + +cat > "$HOME/.rpmmacros" <&1 | grep -Eqi '(pgp|rsa).*signature' + +GNUPGHOME="$metadata_gnupg" gpg --batch --pinentry-mode loopback \ + --passphrase 'metadata-test-passphrase' --quick-generate-key \ + 'OpenCHAMI Test Repository Metadata ' rsa2048 sign 0 +metadata_fingerprint=$(GNUPGHOME="$metadata_gnupg" gpg --batch --with-colons --list-secret-keys | + awk -F: '/^fpr:/ {print $10; exit}') +metadata_key_b64=$(GNUPGHOME="$metadata_gnupg" gpg --batch --pinentry-mode loopback \ + --passphrase 'metadata-test-passphrase' --armor --export-secret-keys "$metadata_fingerprint" | + base64 -w0) + +output_file="$work_dir/action-output" +touch "$output_file" +GITHUB_OUTPUT="$output_file" \ +RUNNER_TEMP="$work_dir" \ +INPUT_RPM_PATH="$test_rpm" \ +INPUT_REPOSITORY_PATH='stable/el9/x86_64' \ +INPUT_PACKAGE_SIGNING_KEYS_PATH="$work_dir/package-keys" \ +INPUT_PACKAGE_MASTER_PUBLIC_KEY_ASC="$package_master_public_key" \ +INPUT_PACKAGE_MASTER_FINGERPRINT="$package_master_fingerprint" \ +INPUT_AUTHORIZED_PACKAGE_REPO_FINGERPRINTS="$package_repo_fingerprint" \ +INPUT_METADATA_SIGNING_KEY_ARMORED_B64="$metadata_key_b64" \ +INPUT_METADATA_SIGNING_KEY_FINGERPRINT="$metadata_fingerprint" \ +INPUT_METADATA_SIGNING_KEY_PASSPHRASE='metadata-test-passphrase' \ +INPUT_REQUIRE_SIGNED_RPMS='true' \ +INPUT_DRY_RUN='true' \ + ./actions/publish-rpm-repository/publish.sh + +repository_dir=$(awk -F= '$1 == "repository-directory" {print substr($0, index($0, "=") + 1)}' "$output_file") +package_count=$(awk -F= '$1 == "package-count" {print $2}' "$output_file") +[[ "$package_count" == '1' ]] +[[ -s "$repository_dir/repodata/repomd.xml" ]] +[[ -s "$repository_dir/repodata/repomd.xml.asc" ]] +[[ -s "$repository_dir/RPM-GPG-KEY-OpenCHAMI" ]] +[[ -s "$repository_dir/RPM-GPG-KEY-OpenCHAMI-Packages" ]] + +verify_gnupg="$work_dir/verify-gnupg" +mkdir -m 700 "$verify_gnupg" +GNUPGHOME="$verify_gnupg" gpg --batch --import "$repository_dir/RPM-GPG-KEY-OpenCHAMI" +GNUPGHOME="$verify_gnupg" gpg --batch --verify \ + "$repository_dir/repodata/repomd.xml.asc" \ + "$repository_dir/repodata/repomd.xml" + +GNUPGHOME="$package_gnupg" gpg --batch --passphrase '' --quick-generate-key \ + 'OpenCHAMI Retired Repository ' rsa2048 cert 0 +retired_repo_fingerprint=$(fingerprint_for_uid 'retired-repo@openchami.org') +GNUPGHOME="$package_gnupg" gpg --batch --yes --quick-sign-key \ + --local-user "$package_master_fingerprint" "$retired_repo_fingerprint" +GNUPGHOME="$package_gnupg" gpg --batch --passphrase '' --quick-generate-key \ + 'OpenCHAMI Rejected Package ' rsa2048 sign 1d +rejected_package_fingerprint=$(fingerprint_for_uid 'rejected-package@openchami.org') +GNUPGHOME="$package_gnupg" gpg --batch --yes --quick-sign-key \ + --local-user "$retired_repo_fingerprint" "$rejected_package_fingerprint" + +rejected_keys_dir="$work_dir/rejected-keys/test-package" +mkdir -p "$rejected_keys_dir" +GNUPGHOME="$package_gnupg" gpg --batch --armor \ + --export "${retired_repo_fingerprint}!" > "$rejected_keys_dir/repo-cert.pub.asc" +GNUPGHOME="$package_gnupg" gpg --batch --armor \ + --export "${rejected_package_fingerprint}!" > "$rejected_keys_dir/ephemeral.pub.asc" + +rejected_output="$work_dir/rejected-output" +touch "$rejected_output" +if GITHUB_OUTPUT="$rejected_output" \ + RUNNER_TEMP="$work_dir" \ + INPUT_RPM_PATH="$test_rpm" \ + INPUT_REPOSITORY_PATH='stable/el9/x86_64' \ + INPUT_PACKAGE_SIGNING_KEYS_PATH="$work_dir/rejected-keys" \ + INPUT_PACKAGE_MASTER_PUBLIC_KEY_ASC="$package_master_public_key" \ + INPUT_PACKAGE_MASTER_FINGERPRINT="$package_master_fingerprint" \ + INPUT_AUTHORIZED_PACKAGE_REPO_FINGERPRINTS="$package_repo_fingerprint" \ + INPUT_METADATA_SIGNING_KEY_ARMORED_B64="$metadata_key_b64" \ + INPUT_METADATA_SIGNING_KEY_FINGERPRINT="$metadata_fingerprint" \ + INPUT_METADATA_SIGNING_KEY_PASSPHRASE='metadata-test-passphrase' \ + INPUT_REQUIRE_SIGNED_RPMS='true' \ + INPUT_DRY_RUN='true' \ + ./actions/publish-rpm-repository/publish.sh; then + echo 'retired package repository key was unexpectedly accepted' >&2 + exit 1 +fi + +echo 'RPM repository publisher dry-run passed'