{"id":51658625,"url":"https://github.com/openchami/gpg-signing-manager","last_synced_at":"2026-07-14T13:03:07.864Z","repository":{"id":348367916,"uuid":"1196509139","full_name":"OpenCHAMI/gpg-signing-manager","owner":"OpenCHAMI","description":"Scripts and instructions to support signing our RPM releases with gnupg","archived":false,"fork":false,"pushed_at":"2026-06-04T18:02:58.000Z","size":60,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T21:06:41.990Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OpenCHAMI.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T19:11:23.000Z","updated_at":"2026-06-04T18:03:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/OpenCHAMI/gpg-signing-manager","commit_stats":null,"previous_names":["openchami/gpg-signing-manager"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/OpenCHAMI/gpg-signing-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenCHAMI%2Fgpg-signing-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenCHAMI%2Fgpg-signing-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenCHAMI%2Fgpg-signing-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenCHAMI%2Fgpg-signing-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OpenCHAMI","download_url":"https://codeload.github.com/OpenCHAMI/gpg-signing-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenCHAMI%2Fgpg-signing-manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35462449,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-14T02:00:06.603Z","response_time":114,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-14T13:03:06.934Z","updated_at":"2026-07-14T13:03:07.856Z","avatar_url":"https://github.com/OpenCHAMI.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GPG Signing Manager\n\nTools for a three-tier GPG signing chain: an **offline master key** certifies\nper-repo signing keys, which certify short-lived **ephemeral release keys**.\nRPMs are signed by the ephemeral key so that every signature is traceable back\nto the master key.\n\n## Trust model\n\n```\noffline master key  (gnupg-master/)\n    └─[certifies]─\u003e repo signing key  (gnupg-repos/\u003crepo\u003e/)\n                        └─[certifies]─\u003e ephemeral release key  (generated in CI)\n                                            └─[signs]─\u003e RPM files\n```\n\nUsers can verify a signed RPM via **any** of the three public keys:\n\n| Public key | Where to get it |\n|---|---|\n| Master | Published by the org (e.g. `MASTER_PUBLIC_ASC` org secret or release artifact) |\n| Repo | Released as `repo-public.asc` alongside RPMs |\n| Ephemeral | Released as `ephemeral-public.asc` alongside RPMs |\n\n## Included\n\n| Path | Purpose |\n|---|---|\n| `scripts/create-master-key.sh` | One-time: create the offline master key |\n| `scripts/create-repo-key.sh` | Per-repo: create a signing key certified by master |\n| `scripts/rotate-repo-key.sh` | Revoke an old repo key and create a replacement |\n| `scripts/verify-chain.sh` | Verify the full master→repo→ephemeral chain locally |\n| `scripts/validate-rotation.sh` | Confirm a rotation is safe before updating secrets |\n| `scripts/list-repo-keys.sh` | Audit all repo keys and their master certifications |\n| `scripts/check-key-expiry.sh` | Fail if a signing key is expired or expiring soon |\n| `actions/check-key-expiration/` | CI: gate a job on key expiry |\n| `actions/gpg-ephemeral-key/` | CI: generate and certify an ephemeral release key |\n| `actions/setup-rpm-signing/` | CI: import key, configure rpmsign |\n| `.github/workflows/example-release.yml` | Copy-paste release workflow template |\n| `.github/workflows/key-expiry-check.yml` | Scheduled key expiry check |\n| `test/integration-test.sh` | End-to-end test of the signing chain |\n\n---\n\n## Prerequisites\n\nInstall the required tools before running any scripts.\n\n```bash\n# macOS (Homebrew)\nbrew install gnupg gh\n\n# Debian / Ubuntu\nsudo apt install gnupg2 gh\n\n# RHEL / Fedora\nsudo dnf install gnupg2 gh\n```\n\nAuthenticate the GitHub CLI once:\n\n```bash\ngh auth login\ngh auth status   # confirm you are logged in\n```\n\n---\n\n## One-time: Create the master key\n\nRun this on an **offline, air-gapped machine**.  The master key never leaves\nthat machine; only the public key and the outputs of `create-repo-key.sh` are\ncopied out.\n\n```bash\n# Clone the repo onto the offline machine, then:\ncd /path/to/gpg-signing-manager\n\nMASTER_FPR=$(scripts/create-master-key.sh \\\n  --name    \"OpenCHAMI Software Signing Key\" \\\n  --email   \"admin@openchami.org\" \\\n  --expire  \"5y\")\n\necho \"Master fingerprint: $MASTER_FPR\"\n```\n\nThe script prints only the fingerprint to stdout; all other output goes to\nstderr so it is safe to capture with `$()`.\n\nAlready have an existing master key in this repo and just need its fingerprint?\n\n```bash\n# Default keyring location used by these scripts:\ngpg --homedir ./gnupg-master --list-secret-keys --with-colons \\\n  | awk -F: '$1==\"fpr\" { print $10; exit }'\n\n# Save it for later commands:\nMASTER_FPR=$(gpg --homedir ./gnupg-master --list-secret-keys --with-colons \\\n  | awk -F: '$1==\"fpr\" { print $10; exit }')\necho \"$MASTER_FPR\"\n```\n\nIf your master keyring is in a different directory, replace `./gnupg-master`\nwith that path.\n\nOutputs in `./gnupg-out/`:\n\n| File | Description |\n|---|---|\n| `master-public.asc` | Public key — safe to publish |\n| `master-secret-backup.asc` | **Keep offline and encrypted** |\n| `\u003cFPR\u003e-revocation.asc` | Store securely offline |\n\nPublish the master public key as an org-level GitHub secret and note the\nfingerprint:\n\n```bash\nORG=\"your-org\"\n\ngh secret set MASTER_PUBLIC_ASC \\\n  --org \"$ORG\" \\\n  --visibility all \\\n  \u003c gnupg-out/master-public.asc\n\ngh secret set MASTER_FPR \\\n  --org \"$ORG\" \\\n  --visibility all \\\n  --body \"$MASTER_FPR\"\n```\n\n---\n\n## Per-repo: Create and store a repo signing key\n\nRun this on the **offline machine** where the master key lives.\n\n```bash\nREPO=\"your-org/your-repo\"\n\nscripts/create-repo-key.sh \\\n  --master-fpr \"$MASTER_FPR\" \\\n  --repo       \"$REPO\"\n```\n\nDefault paths: master keyring → `./gnupg-master`, repo keyring →\n`./gnupg-repos/\u003crepo\u003e`, outputs → `./gnupg-out/\u003crepo\u003e`.\n\nThe script creates **two separate secret artifacts** with different purposes:\n\n| Artifact | GitHub Secret | Purpose |\n|---|---|---|\n| `\u003crepo\u003e-secret-subkeys.b64` | `GPG_REPO_KEY_B64` | Signs RPMs via `setup-rpm-signing` |\n| `\u003crepo\u003e-secret-cert.b64` | `GPG_REPO_CERT_KEY_B64` | Certifies ephemeral keys via `gpg-ephemeral-key` |\n\nNeither artifact contains master key material.\n\nCopy both files to an online machine (or use a USB drive), then set the secrets:\n\n```bash\nREPO=\"your-org/your-repo\"\nSAFE_REPO=\"${REPO//\\//-}\"   # e.g. \"your-org-your-repo\"\nOUTDIR=\"$(pwd)/gnupg-out/${SAFE_REPO}\"\n\n# Signs RPMs in CI.\ngh secret set GPG_REPO_KEY_B64 \\\n  --repo \"$REPO\" \\\n  \u003c \"${OUTDIR}/${SAFE_REPO}-secret-subkeys.b64\"\n\n# Certifies ephemeral release keys in CI.\ngh secret set GPG_REPO_CERT_KEY_B64 \\\n  --repo \"$REPO\" \\\n  \u003c \"${OUTDIR}/${SAFE_REPO}-secret-cert.b64\"\n```\n\n\u003e **Security note**  `GPG_REPO_KEY_B64` contains only signing-capable subkey\n\u003e material — it cannot certify any key.  `GPG_REPO_CERT_KEY_B64` contains only\n\u003e the cert-capable primary key with no signing subkeys.  Keeping them separate\n\u003e limits the blast radius of a secret exposure.\n\nAlso publish the certified repo public key alongside your releases:\n\n```bash\n# The certified public key is at:\necho \"${OUTDIR}/${SAFE_REPO}-public.asc\"\n```\n\n---\n\n## CI usage\n\n### `gpg-ephemeral-key` — generate a per-release signing key\n\nGenerates a short-lived Ed25519 key, certifies it with `GPG_REPO_CERT_KEY_B64`,\nand exports the public key.  **The job fails if certification cannot be\ncompleted**, ensuring the trust chain is always enforced.\n\n```yaml\n- name: Generate ephemeral release key\n  id: ephemeral\n  uses: ./actions/gpg-ephemeral-key\n  with:\n    cert-key-armored-b64: ${{ secrets.GPG_REPO_CERT_KEY_B64 }}\n    name:    '${{ github.repository }} Release'\n    comment: 'ephemeral key for ${{ github.ref_name }}'\n    email:   'release@packages.openchami.org'\n    expire:  '24h'\n```\n\nOutputs:\n\n| Output | Description |\n|---|---|\n| `ephemeral-fingerprint` | Full fingerprint of the generated key |\n| `ephemeral-public-key` | Base64-encoded ASCII-armored public key |\n| `repo-cert-keyid` | Short key ID used for certification |\n\n### `setup-rpm-signing` — configure rpmsign\n\nImports the repo signing key and writes `~/.rpmmacros`.  Pass\n`ephemeral-fingerprint` to sign RPMs with the ephemeral key instead of the\nrepo key directly.\n\n```yaml\n- name: Setup RPM signing\n  id: rpm_signing\n  uses: ./actions/setup-rpm-signing\n  with:\n    repo-key-armored-b64:  ${{ secrets.GPG_REPO_KEY_B64 }}\n    ephemeral-fingerprint: ${{ steps.ephemeral.outputs.ephemeral-fingerprint }}\n    public-key-output:     repo-public.asc\n\n# Then sign your RPMs:\n- name: Sign RPMs\n  run: |\n    for rpm in dist/*.rpm; do\n      rpm --define \"_gpg_name ${{ steps.ephemeral.outputs.ephemeral-fingerprint }}\" \\\n          --addsign \"$rpm\"\n    done\n```\n\n### `check-key-expiration` — gate a job on key health\n\n```yaml\nname: Key expiry check\n\non:\n  schedule:\n    - cron: '17 6 * * 1'   # every Monday morning\n  workflow_dispatch:\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Check repo key expiration\n        uses: ./actions/check-key-expiration\n        with:\n          # Backward-compatible fallback while migrating old repos.\n          key-armored-b64: ${{ secrets.GPG_REPO_KEY_B64 || secrets.GPG_SUBKEY_B64 }}\n          secret-name: 'GPG_REPO_KEY_B64'\n          legacy-secret-name: 'GPG_SUBKEY_B64'\n          warn-days: '30'\n```\n\nIf this action fails with `Input 'key-armored-b64' is empty`, the expected\nsecret is missing.  Set `GPG_REPO_KEY_B64` (preferred) or temporarily keep\n`GPG_SUBKEY_B64` wired through the fallback expression above.\n\n---\n\n## Full release workflow\n\nSee `.github/workflows/example-release.yml` for a complete, copy-paste-ready\nrelease workflow that wires all three actions together and runs `verify-chain.sh`\nas a gate before publishing.\n\n---\n\n## Verifying a release locally\n\nDownload `repo-public.asc` and `ephemeral-public.asc` from the GitHub Release\npage, then run:\n\n```bash\n# Basic: import and verify RPM signatures\ngpg --import repo-public.asc ephemeral-public.asc\nrpm --checksig *.rpm\n\n# Full chain verification (also validates master→repo→ephemeral certifications)\n# Requires the master public key (available as MASTER_PUBLIC_ASC org secret\n# or from the offline machine's gnupg-out/master-public.asc).\nscripts/verify-chain.sh \\\n  --master    master-public.asc \\\n  --repo      repo-public.asc \\\n  --ephemeral ephemeral-public.asc \\\n  --rpm       *.rpm\n```\n\n`verify-chain.sh` prints `PASS` or `FAIL` for each check and exits non-zero\nif any check fails.\n\n---\n\n## Rotating a repo key\n\nRun this on the **offline machine** where the master key lives.\n\n```bash\nREPO=\"your-org/your-repo\"\nOLD_REPO_FPR=\"\u003cfingerprint of the key being replaced\u003e\"\n\nscripts/rotate-repo-key.sh \\\n  --master-fpr   \"$MASTER_FPR\" \\\n  --old-repo-fpr \"$OLD_REPO_FPR\" \\\n  --repo         \"$REPO\"\n```\n\nOutputs appear in `./gnupg-out/\u003crepo\u003e-rotation/`.\n\nBefore updating GitHub Secrets, validate that the rotation is safe:\n\n```bash\nSAFE_REPO=\"${REPO//\\//-}\"\nROTATION_DIR=\"gnupg-out/${SAFE_REPO}-rotation\"\n\nscripts/validate-rotation.sh \\\n  --master-fpr \"$MASTER_FPR\" \\\n  --old-key    \"${ROTATION_DIR}/${SAFE_REPO}-old-public-revoked.asc\" \\\n  --new-key    \"${ROTATION_DIR}/new-key/${SAFE_REPO}-public.asc\"\n```\n\nWhen `validate-rotation.sh` exits 0, update both GitHub Secrets:\n\n```bash\nNEW_KEY_DIR=\"${ROTATION_DIR}/new-key\"\n\ngh secret set GPG_REPO_KEY_B64 \\\n  --repo \"$REPO\" \\\n  \u003c \"${NEW_KEY_DIR}/${SAFE_REPO}-secret-subkeys.b64\"\n\ngh secret set GPG_REPO_CERT_KEY_B64 \\\n  --repo \"$REPO\" \\\n  \u003c \"${NEW_KEY_DIR}/${SAFE_REPO}-secret-cert.b64\"\n```\n\nAlso publish the new certified public key and distribute the revocation\ncertificate for the old key.\n\n---\n\n## Running the test suite\n\n```bash\nbash test/integration-test.sh\n```\n\nExpected output:\n\n```\n======================================================\n  GPG Signing Chain Integration Test\n======================================================\n  Working directory: /tmp/tmp.XXXXXXXXXX\n\n--- Test 1: Create master key ---\n  Test 1 PASS: Master key created (FINGERPRINT)\n\n--- Test 2: Create repo key with dual secret exports ---\n  Test 2 PASS: Repo key created (FINGERPRINT), both artifacts exported, master cert present\n\n--- Test 3: Generate and certify ephemeral Ed25519 key ---\n  Test 3 PASS: Ephemeral key (FINGERPRINT) certified by repo key (FINGERPRINT)\n\n--- Test 4: verify-chain.sh validates full chain ---\n  Test 4 PASS: verify-chain.sh exited 0 — full chain is valid\n\n--- Test 5: verify-chain.sh rejects uncertified ephemeral key ---\n  Test 5 PASS: verify-chain.sh correctly rejected an uncertified ephemeral key (exited non-zero)\n\n--- Test 6: verify-chain.sh rejects repo key not signed by master ---\n  Test 6 PASS: verify-chain.sh correctly rejected a broken chain (exited non-zero)\n\n======================================================\n  Results: 6 passed, 0 failed, 0 skipped\n======================================================\n  All tests passed.\n```\n\nCI runs the same suite on every push via `.github/workflows/test-scripts.yml`.\n\n---\n\n## Running workflows locally with act\n\n[`act`](https://github.com/nektos/act) runs GitHub Actions workflows in local\nDocker containers (Colima works as the container runtime).\n\n### Install\n\n```bash\nbrew install act\n```\n\n### One-time setup\n\nCopy `.secrets.example` to `.secrets` and fill in real key material.  `.secrets`\nis gitignored and must never be committed.\n\n```bash\ncp .secrets.example .secrets\n# edit .secrets — paste the base64 key values produced by create-repo-key.sh\n```\n\nThe `.actrc` file in the repo root configures `act` to:\n- use `ghcr.io/catthehacker/ubuntu:act-latest`\n- read secrets from `.secrets`\n- target `linux/arm64` by default on Apple Silicon (lower memory than emulated amd64)\n\nThis image is much smaller than `full-latest`.  It is sufficient here because\nthe workflows install `gnupg2`, `rpm`, and `shellcheck` explicitly.\n\nIf you need amd64 parity for a specific workflow, override per run:\n\n```bash\nact push -W .github/workflows/test-scripts.yml --container-architecture linux/amd64\n```\n\n### Run individual workflows\n\n```bash\n# Shellcheck + integration tests (no secrets required)\nact push -W .github/workflows/test-scripts.yml\n\n# Scheduled key expiry check (requires GPG_REPO_KEY_B64 in .secrets)\nact schedule -W .github/workflows/key-expiry-check.yml\n\n# Full release workflow (requires all three secrets in .secrets)\n# The example-release workflow triggers on tag pushes or workflow_dispatch.\nact workflow_dispatch -W .github/workflows/example-release.yml\n```\n\n### Dry-run (list jobs without running containers)\n\n```bash\nact push -W .github/workflows/test-scripts.yml --list\n```\n\n### Testing the composite actions directly\n\nThe composite actions (`check-key-expiration`, `gpg-ephemeral-key`,\n`setup-rpm-signing`) are invoked from workflows rather than run standalone.\nUse the release workflow as the harness:\n\n```bash\nact workflow_dispatch -W .github/workflows/example-release.yml\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenchami%2Fgpg-signing-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenchami%2Fgpg-signing-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenchami%2Fgpg-signing-manager/lists"}