Short answer. With Sigstore "keyless" signing, the signer proves an identity with an OIDC token (a GitHub
Actions workflow, a Google or Microsoft account). Fulcio issues a certificate for an ephemeral key, valid for
about ten minutes, with that identity inside. The signature is recorded in the public Rekor transparency log,
and everything needed to verify ends up in a bundle (.sigstore.json). Verification has to check who
signed, not just that the signature is valid. In cosign that means --certificate-identity and
--certificate-oidc-issuer, which cosign requires in keyless mode. npm provenance and GitHub artifact
attestations are the same machinery: npm audit signatures and gh attestation verify check them for you.
Why keyless
Classic release signing with GPG or a long-lived code-signing key has two chronic problems. Users have to know which key is the right one, so key distribution is the actual trust problem. And the key must be stored somewhere for years, usually a CI secret, where it can leak. Sigstore replaces the key with an identity people already understand, such as "the release workflow of repository X" or "alice@example.com". It replaces the key's lifetime with a log entry that proves when the signature was made.
Sigstore consists of public services run by the OpenSSF community (the "public-good instance") and client tools:
- Fulcio is a certificate authority that issues short-lived X.509 certificates for OIDC identities.
- Rekor is the transparency log for signatures and attestations.
- TUF metadata distributes the roots of trust (Fulcio's CA, Rekor's key, timestamp authorities) to clients.
- cosign is the CLI for blobs and container images. sigstore-python, sigstore-js, sigstore-go and sigstore-java are libraries for the same tasks.
What happens when you sign
- The client creates an ephemeral key pair in memory.
- It gets an OIDC token: in GitHub Actions from the runner (
id-token: write), on a laptop through a browser login. - Fulcio checks the token and issues a certificate binding the public key to the identity: the email or workflow URI goes into the Subject Alternative Name, the issuer and CI details into extensions.
- The client signs the artifact's digest and submits signature and certificate to Rekor, which returns a log entry with an inclusion proof and a signed timestamp.
- The ephemeral private key is discarded. The bundle stores certificate, signature and log evidence.
The certificate in the bundle that cosign ships with its own v3.1.3 release shows how short-lived it is:
issuer=O=sigstore.dev, CN=sigstore-intermediate
notBefore=Aug 6 01:03:25 2026 GMT
notAfter=Aug 6 01:13:25 2026 GMT
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Alternative Name: critical
email:keyless@projectsigstore.iam.gserviceaccount.com
1.3.6.1.4.1.57264.1.1:
https://accounts.google.com
Ten minutes of validity. The signature stays verifiable after that, because the Rekor entry proves it was made
inside that window. The Rekor integratedTime of this entry is 2026-08-06 01:03:25 UTC.
The bundle
A bundle is a JSON document that contains everything a verifier needs except the artifact and the trust roots. This is the cosign release bundle, abridged:
{
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"verificationMaterial": {
"certificate": { "rawBytes": "MIIDDzCCApWgAwIBAgIUavXJ..." },
"tlogEntries": [
{
"logIndex": "2352242192",
"kindVersion": { "kind": "hashedrekord", "version": "0.0.1" },
"integratedTime": "1785978205",
"inclusionPromise": { "signedEntryTimestamp": "MEYCIQCvddr55BoX..." },
"inclusionProof": { "logIndex": "2230337930", "treeSize": "2230337938", "rootHash": "62m7TRzwvcKyu+r6...", "hashes": ["..."], "checkpoint": { "envelope": "..." } },
"canonicalizedBody": "eyJhcGlWZXJzaW9u..."
}
],
"timestampVerificationData": { "rfc3161Timestamps": ["..."] }
},
"messageSignature": {
"messageDigest": { "algorithm": "SHA2_256", "digest": "xdMk4JGCaw16eOsW..." },
"signature": "MEUCIECQp5FyO00l..."
}
}
certificateis the Fulcio leaf, with the identity.tlogEntriesholds the Rekor entry: an inclusion proof against a signed checkpoint and a signed entry timestamp. With these, a verifier does not need to contact Rekor at all.timestampVerificationDataholds optional RFC 3161 timestamps from a timestamp authority.- The content is either
messageSignaturefor a plain blob signature ordsseEnvelopefor an attestation, a signed in-toto statement.
The Sigstore bundle verifier checks all of these parts in the browser and shows the identity in the certificate, which is the first thing to look at when a verification fails.
Verifying: identity is the point
cosign verifying its own release binary (ghcr.io/sigstore/cosign/cosign:v3.1.3, 20 October 2026). The release is
built on Google infrastructure, so the identity is a Google service account and the issuer is Google, not GitHub:
$ cosign verify-blob cosign-linux-arm64 --bundle cosign-linux-arm64.sigstore.json \
--certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com
Verified OK
Now the failure cases. Each one has a message that tells you exactly which check failed:
--- right email, wrong issuer
error during command execution: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected issuer value "https://token.actions.githubusercontent.com", got "https://accounts.google.com"
--- a different identity
error during command execution: failed to verify certificate identity: no matching CertificateIdentity found, last error: expected SAN value "release@example.com", got "keyless@projectsigstore.iam.gserviceaccount.com"
--- no identity at all
error during command execution: --certificate-identity or --certificate-identity-regexp is required for verification in keyless mode
--- a modified binary
error during command execution: failed to verify signature: could not verify message: invalid signature when validating ASN.1 encoded signature
The third case is deliberate design. Anyone can get a Fulcio certificate for their own identity and sign anything. A signature is only meaningful once you check that the identity is the one you expect. cosign refuses to verify keyless signatures without an identity (error page). The last case is what tampering looks like (error page).
Where do you get the expected identity? From the project's documentation, or by reading it once from a bundle you
trust and pinning it in your verification script. --certificate-identity-regexp is useful for tags (for
example ...release.yml@refs/tags/v.*), but anchor it at both ends. A pattern like .*sigstore.* matches
identities that anyone can create.
Signing in GitHub Actions
In CI the identity is the workflow. Fulcio puts the workflow file and ref into the SAN, and the details into
extensions. The npm provenance bundle of sigstore@5.0.0 shows what a GitHub Actions certificate carries,
decoded with a small script from its X.509 extensions:
1.3.6.1.4.1.57264.1.8 Issuer (V2) https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.9 Build Signer URI https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main
1.3.6.1.4.1.57264.1.12 Source Repository URI https://github.com/sigstore/sigstore-js
1.3.6.1.4.1.57264.1.13 Source Repository Digest 7d2900eca1c22b3f87c13987c8d4b7c9a29b733a
1.3.6.1.4.1.57264.1.14 Source Repository Ref refs/heads/main
1.3.6.1.4.1.57264.1.18 Build Config URI https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main
1.3.6.1.4.1.57264.1.20 Build Trigger push
1.3.6.1.4.1.57264.1.21 Run Invocation URI https://github.com/sigstore/sigstore-js/actions/runs/26781866618/attempts/1
cosign can check these claims too, in addition to the SAN:
$ cosign verify-blob-attestation --bundle provenance.sigstore.json --type slsaprovenance1 \
--certificate-identity-regexp '^https://github\.com/sigstore/sigstore-js/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-github-workflow-repository sigstore/sigstore-js \
--certificate-github-workflow-trigger push \
sigstore-5.0.0.tgz
Verified OK
To sign your own releases in GitHub Actions, the job needs permission to request an OIDC token. GitHub's
actions/attest README gives this minimal setup for build provenance of a
file:
permissions:
id-token: write
attestations: write
artifact-metadata: write
steps:
# ... build steps that produce dist/app.tar.gz ...
- uses: actions/attest@v4
with:
subject-path: 'dist/app.tar.gz'
Public repositories use the public-good Sigstore instance. Private repositories use GitHub's own instance, which
requires GitHub Enterprise Cloud. actions/attest-build-provenance still works, but since v4 it is only a
wrapper around actions/attest. To sign a blob with cosign directly in the job, the command is
cosign sign-blob --bundle app.sigstore.json app.tar.gz --yes. cosign's --oidc-provider help lists
github-actions among the token sources it tries when none is set. We did not run a keyless signing for this
page, because every keyless signature on the public instance is a permanent, public log entry.
A signature from a workflow is only as trustworthy as the workflow. Anyone who can push to the branch, or trigger
the workflow, can produce a valid signature with that identity. Protect release workflows with branch and tag
protection, required reviews and environment approvals, and pin verification to a tag ref
(@refs/tags/...) when you release from tags.
npm provenance
npm publishes two kinds of Sigstore evidence for packages built in supported CI: a publish attestation signed
by the registry and a SLSA provenance attestation signed by the build. npm audit signatures checks both
for everything installed:
$ npm audit signatures
audited 52 packages in 2s
52 packages have verified registry signatures
26 packages have verified attestations
(use --json --include-attestations to view attestation details)
The attestations for one version are public, at https://registry.npmjs.org/-/npm/v1/attestations/<name>@<version>.
For sigstore@5.0.0:
https://github.com/npm/attestation/tree/main/specs/publish/v0.1 | bundle application/vnd.dev.sigstore.bundle+json;version=0.2
subject: pkg:npm/sigstore@5.0.0 849a897e81bf7b8a8541a6ae...
https://slsa.dev/provenance/v1 | bundle application/vnd.dev.sigstore.bundle.v0.3+json
subject: pkg:npm/sigstore@5.0.0 849a897e81bf7b8a8541a6ae...
workflow: https://github.com/sigstore/sigstore-js .github/workflows/release.yml refs/heads/main
builder: https://github.com/actions/runner/github-hosted
commit: 7d2900eca1c22b3f87c13987c8d4b7c9a29b733a
The subject digest is the SHA-512 of the tarball (849a897e...), so the provenance bundle can be verified
against the downloaded .tgz with cosign, as shown above. With npm's
trusted publishing (npm CLI 11.5.1 or later), packages published
from GitHub Actions or GitLab CI through OIDC get provenance automatically, without the --provenance flag and
without a long-lived npm token. CircleCI is supported for trusted publishing but not yet for provenance. Outside a
supported CI, npm publish --provenance fails with
Automatic provenance generation not supported for provider: null.
The npm provenance checker shows the repository, workflow and
commit for any package version.
GitHub artifact attestations
GitHub stores attestations for release assets and makes them available through its API. The gh CLI release
tarball gh_2.102.0_linux_arm64.tar.gz (SHA-256 7862c86c...) had two attestations on 20 October 2026: SLSA
provenance from the cli/cli deployment workflow, and a release attestation signed by GitHub itself. With the
provenance bundle saved locally, gh attestation verify works without logging in and prints the policy it
enforced:
$ gh attestation verify gh_2.102.0_linux_arm64.tar.gz --repo cli/cli --bundle gh-1.sigstore.json
Loaded digest sha256:7862c86c72f43df3a2d93ddde6f473285b4e2af61b494849846827e513ef6484 for file:///d/gh_2.102.0_linux_arm64.tar.gz
Loaded 1 attestations from /d/gh-1.sigstore.json
The following policy criteria will be enforced:
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Source Repository Owner URI must match:... https://github.com/cli
- Source Repository URI must match:......... https://github.com/cli/cli
- Subject Alternative Name must match regex: (?i)^https://github\.com/cli/cli/
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
✓ Verification succeeded!
The following 1 attestation matched the policy criteria
- Attestation #1
- Build repo:..... cli/cli
- Build workflow:. .github/workflows/deployment.yml@refs/heads/trunk
- Signer repo:.... cli/cli
- Signer workflow: .github/workflows/deployment.yml@refs/heads/trunk
With --repo example/cli the same check ends in ✗ Sigstore verification failed. The default policy (owner,
repository, SAN prefix) is a good start. For release automation, add --signer-workflow to pin the exact
workflow file, and --deny-self-hosted-runners if your builds run only on GitHub-hosted runners. The same bundle
also verifies with cosign: cosign verify-blob-attestation --type slsaprovenance1 with the SAN
https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk printed Verified OK.
The second attestation is different. Its predicate type is https://in-toto.io/attestation/release/v0.2. The
certificate names URI:https://dotcom.releases.github.com and was issued by O=GitHub, Inc., CN=Fulcio Intermediate l1, GitHub's own Fulcio, with an RFC 3161 timestamp instead of a Rekor entry. GitHub creates it for
repositories with immutable releases enabled. It proves the asset belongs to the release, not how it was built.
gh release verify and gh release verify-asset check it. In our test gh release verify-asset needed a
logged-in gh: without a token, gh 2.102.0 stopped with To get started with GitHub CLI, please run: gh auth login.
When keyless does not fit
Air-gapped builds, private artifacts that must not appear in a public log, or policies that require a key in an HSM: cosign also signs with keys, and the log upload can be switched off. Tested with cosign v3.1.3:
$ cosign generate-key-pair
Private key written to cosign.key
Public key written to cosign.pub
$ cosign sign-blob --key cosign.key --tlog-upload=false --use-signing-config=false --bundle app.sigstore.json app-1.0.0.tar.gz --yes
Using payload from: app-1.0.0.tar.gz
Signing artifact...
Wrote bundle to file app.sigstore.json
$ cosign verify-blob --key cosign.pub --bundle app.sigstore.json --insecure-ignore-tlog=true app-1.0.0.tar.gz
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the blob.
Verified OK
$ cosign verify-blob --key cosign.pub --bundle app.sigstore.json app-1.0.0.tar.gz
error during command execution: failed to verify log inclusion: not enough verified log entries from transparency log: 0 < 1
The bundle then contains only a publicKey hint, and you are back to distributing and protecting a long-lived
key, the problem keyless signing removes. For a private but still auditable setup, run your own Sigstore
instance (Fulcio, Rekor, TUF) or use a vendor's. GitHub does this for private repositories.
The state of the infrastructure in 2026
- Rekor v2, a tile-based log, reached general availability in October 2025. Verifiers need cosign 2.6.0 or 3.0.1 and later. Rekor v2 dropped the search index.
- In June 2026 the maintainers announced that the public-good instance "will continue using Rekor v1 as the default log for the foreseeable future". Rekor v2 stays in production for ecosystems that opt in.
- A separate post-quantum Sigstore with ML-DSA is "targeted for mid-2027", per the October 2026 announcement. The classical instance is to keep running until a turndown "tentatively scheduled for 2029 or 2030". Bundles stay the format either way.
The verification side barely changes: keep your clients current, verify from bundles, and pin identities, not log URLs.
Common mistakes
- Checking the signature but not the identity. A valid Sigstore signature only says someone signed.
- Over-broad identity patterns. Unanchored regular expressions, or an issuer check without a SAN check.
- Pinning to a branch when you release from tags.
@refs/heads/mainaccepts anything built frommain, including a build nobody meant to release. - Assuming signing proves the build. A blob signature proves who signed the bytes. Provenance (SLSA, in-toto) says how and from what they were built. Verify the one your policy needs.
- Signing personal releases with a personal email without realising that the email is in the certificate and therefore in a public, permanent log.
- Shipping detached
.sigand.pemfiles from older cosign versions. Publish the.sigstore.jsonbundle. It is the format all current verifiers understand.
Sources
- Sigstore documentation and the bundle format.
- cosign v3.1.3 and its release bundles.
- Sigstore blog: Rekor v2 GA (October 2025), sigstore.dev and Rekor evolution (June 2026), Post-Quantum Cryptography for Sigstore (October 2026).
- npm: trusted publishing and the registry attestations endpoint.
- GitHub:
actions/attest,gh attestation verify,gh release verify, and the in-toto release predicate.
Verification ran on 20 October 2026 in ghcr.io/sigstore/cosign/cosign:v3.1.3, with npm 11.19.1 and gh 2.102.0
(the official release tarball) in node:24.22.0-trixie.