Skip to content
ecdsa.com

Sigstore vs GPG: signing software releases with or without long-lived keys

A GPG signature says that the holder of a particular key signed. A Sigstore signature says that a particular identity, often a CI workflow, signed at a logged moment, with a key that existed for minutes.

Sigstore vs GPG is the choice between two ways to sign release artifacts, containers and packages. With OpenPGP (GnuPG and compatible tools), a maintainer keeps a long-lived private key, publishes its fingerprint, and ships a detached signature next to each file. With Sigstore, the signer authenticates with OpenID Connect, the Fulcio certificate authority issues a certificate binding a fresh key to that identity for ten minutes, the signature is recorded in the Rekor transparency log, and the key is thrown away. Verifiers check an identity, not a key.

At a glance

Sigstore (keyless)OpenPGP (GPG)
What the verifier trustsAn OIDC identity (e-mail or workflow URI) and issuer, via Fulcio's rootA specific public key, obtained and checked out of band
Signing key lifetimeEphemeral; certificate valid 10 minutesYears; expiry set and extended by the owner
Key managementNone for signersGenerate, back up, protect, rotate, revoke
Proof of signing timeRekor log entry (and optional RFC 3161 timestamp)Signature creation time stated by the signer
TransparencyEvery signature publicly loggedNone
RevocationNot needed for ephemeral keys; compromised identities are handled by policyRevocation certificate the owner must publish
Offline signingNo (needs Fulcio and Rekor, public or private)Yes
Artifact formatSigstore bundle (.sigstore.json): certificate, signature or DSSE envelope, log proof.asc or .sig file
Typical toolscosign, sigstore-python, sigstore-js, gitsign, GitHub attest-build-provenancegpg, Sequoia (sq, sqv), rpm and apt tooling
Where it is standardnpm provenance (GA September 2023), PyPI attestations (November 2024), GitHub artifact attestations (GA June 2024), CPython since 3.11 (only option from 3.14), Kubernetes and many container imagesMaven Central (required), Debian and RPM repositories, Linux kernel tags, many source tarballs
PrivacySigner identity (e-mail or repository) is public in the logKey and user ID public only if the signer publishes them

What a keyless signature contains

npm stores Sigstore bundles for packages published with provenance. The script below fetches the attestations for @noble/curves 2.3.0 from the public registry API and decodes the signing certificate (20 October 2026, curl, jq 1.8.1, OpenSSL 3.6.5).

Inspect the Sigstore bundle behind an npm provenance attestation
#!/bin/sh
curl -s "https://registry.npmjs.org/-/npm/v1/attestations/@noble%2fcurves@2.3.0" -o att.json
jq -r '.attestations[].predicateType' att.json
bundle='.attestations[] | select(.predicateType == "https://slsa.dev/provenance/v1") | .bundle'
jq -r "$bundle | .mediaType" att.json
jq -r "$bundle | .verificationMaterial.tlogEntries[0] | \"Rekor log index \(.logIndex), integrated \(.integratedTime | tonumber | todate)\"" att.json
jq -r "$bundle | .verificationMaterial.certificate.rawBytes" att.json | openssl base64 -d -A > leaf.der
openssl x509 -inform DER -in leaf.der -noout -issuer -dates -ext subjectAltName
openssl x509 -inform DER -in leaf.der -noout -text | grep -A1 "57264.1.1:" | tail -1 | sed 's/^ */OIDC issuer: /'
rm -f att.json leaf.der
Output
https://github.com/npm/attestation/tree/main/specs/publish/v0.1
https://slsa.dev/provenance/v1
application/vnd.dev.sigstore.bundle.v0.3+json
Rekor log index 2357213216, integrated 2026-08-06T07:37:48Z
issuer=O=sigstore.dev, CN=sigstore-intermediate
notBefore=Aug  6 07:37:48 2026 GMT
notAfter=Aug  6 07:47:48 2026 GMT
X509v3 Subject Alternative Name: critical
    URI:https://github.com/paulmillr/jsbt/.github/workflows/release.yml@5320e209ca8cc9b1b0b569675e164bdd2a139d71
OIDC issuer: https://token.actions.githubusercontent.com

The package carries two attestations: npm's own publish attestation and a SLSA provenance statement signed in CI. The certificate is valid for exactly ten minutes and was logged in Rekor the second it was issued. Its subject is not a person or a key but the workflow that ran, here a shared release workflow in another repository pinned to a commit; further Fulcio extensions record the source repository, commit, tag and run. The Sigstore bundle verifier and npm provenance checker decode the same data for any package.

How verification differs

Verifying a GPG signature is one command, gpg --verify file.asc file, but its meaning depends on everything before it: how the verifier obtained the key, whether the fingerprint was checked against a trustworthy source, and whether the key has since been revoked. Most "Good signature" results in CI pipelines trust whatever key was imported a few lines earlier.

Verifying a Sigstore signature requires naming the expected identity, which forces the policy question into the command. The script below checks the checksums file of cosign's own v3.1.3 release with the official cosign v3.1.3 container image three times: against the identity the Sigstore project documents for its releases (a Google service account, issuer https://accounts.google.com), against a GitHub Actions workflow identity that did not sign the file, and after appending one byte to the file.

Keyless verification of a public release file with cosign v3.1.3
url=https://github.com/sigstore/cosign/releases/download/v3.1.3
curl -sSLO "$url/cosign_checksums.txt"
curl -sSLO "$url/cosign_checksums.txt.sigstore.json"
cosign() { docker run --rm -v "$PWD":/work:ro -w /work ghcr.io/sigstore/cosign/cosign:v3.1.3 "$@"; }

echo "== expected identity"
cosign verify-blob cosign_checksums.txt \
  --bundle cosign_checksums.txt.sigstore.json \
  --certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
  --certificate-oidc-issuer https://accounts.google.com

echo "== any other identity"
cosign verify-blob cosign_checksums.txt \
  --bundle cosign_checksums.txt.sigstore.json \
  --certificate-identity "https://github.com/sigstore/cosign/.github/workflows/release.yml@refs/tags/v3.1.3" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

echo "== modified file"
printf 'x' >> cosign_checksums.txt
cosign verify-blob cosign_checksums.txt \
  --bundle cosign_checksums.txt.sigstore.json \
  --certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
  --certificate-oidc-issuer https://accounts.google.com
Output (20 October 2026)
== expected identity
Verified OK
== any other identity
Error: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "https://github.com/sigstore/cosign/.github/workflows/release.yml@refs/tags/v3.1.3", got "keyless@projectsigstore.iam.gserviceaccount.com"
error during command execution: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "https://github.com/sigstore/cosign/.github/workflows/release.yml@refs/tags/v3.1.3", got "keyless@projectsigstore.iam.gserviceaccount.com"
== modified file
Error: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
error during command execution: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature

For the first result cosign checked the certificate chain to the Sigstore root (distributed through TUF), the signature over the file, the Rekor inclusion proof and that the log time falls inside the certificate's ten-minute window. The second run is the point of identity pinning: the signature is cryptographically valid, but it was not made by the identity the verifier asked for, so it is rejected; the third fails on the signature itself. For artifacts built in GitHub Actions the identity is the workflow file with its ref, such as the release.yml@5320e209… workflow in the npm provenance certificate above, and the issuer is https://token.actions.githubusercontent.com.

Package ecosystems wrap these checks in their own tools. npm verifies both its registry signatures and the Sigstore attestations of everything installed:

Verify registry signatures and provenance of installed npm packages
cd "$(mktemp -d)" && npm init -y > /dev/null
npm install --ignore-scripts --no-fund --no-audit @noble/curves@2.3.0 @noble/hashes@2.3.0
npm audit signatures
Output
added 2 packages in 589ms
audited 2 packages in 0s

2 packages have verified registry signatures

2 packages have verified attestations
(use --json --include-attestations to view attestation details)

gh attestation verify does the same for GitHub artifact attestations, and PyPI checks attestations against the project's trusted publisher when they are uploaded.

Trade-offs to know

  • Trust moves to identity providers and Sigstore's infrastructure: an attacker who controls the maintainer's GitHub account or the release workflow can obtain valid signatures. Transparency makes this detectable (maintainers can monitor the log for their identity), not impossible.
  • Public metadata: personal e-mail identities appear in the public log; workflow identities reveal repository names. Private Sigstore deployments avoid both.
  • Availability: signing needs Fulcio and Rekor online. Verification of a bundle works offline once the trust root is cached.
  • Operations evolve: Rekor v2, a tile-based log, has been generally available since October 2025, but the public instance keeps Rekor v1 as its default while maintainers work on post-quantum readiness.
  • GPG's costs are human: keys get lost, expire unnoticed or are never verified by downstream users, which is the main reason Python, npm and PyPI moved away from it.

When to choose which

  • Packages built in GitHub Actions or GitLab CI for npm, PyPI, crates or containers: Sigstore via trusted publishing and provenance.
  • Container images: cosign signatures and attestations, verified by admission policies.
  • Maven Central: OpenPGP is required; add a Sigstore bundle as well, which Central has validated since January 2025.
  • Linux distribution packages and repositories: OpenPGP, as the distribution tooling expects.
  • Air-gapped or offline signing: OpenPGP (or a private Sigstore deployment with your own identity provider).
  • Git commits and tags: SSH or OpenPGP keys, or gitsign for keyless commits (see SSH vs GPG commit signing).

Migration notes

  1. Sign in CI, not on laptops: move releases to a workflow with an OIDC token (id-token: write in GitHub Actions) and sign there.
  2. Publish both for a transition period: ship .asc and .sigstore.json files, and document the expected identity and issuer next to the old key fingerprint.
  3. Use registry-native mechanisms where they exist: npm --provenance and trusted publishing, PyPI trusted publishers, GitHub artifact attestations.
  4. Pin identities precisely in verification policies: repository, workflow file and tag or branch, not just the organization.
  5. Monitor the log for your identities, and keep your OpenPGP key's revocation certificate until the last release signed with it is no longer supported.

References

All comparisons