Back to glossary

Sigstore

Sigstore signs software with short-lived certificates tied to an OIDC identity and logs every signature in the public Rekor transparency log.

Also known as Sigstore keyless signing, keyless signing, cosign, Fulcio, Rekor, Sigstore bundle, Sigstore provenance

Sigstore is an open source signing system that replaces long-lived private keys with short-lived, identity-bound certificates. A signer proves its identity through OpenID Connect, the Fulcio certificate authority issues a 10-minute certificate, and every signature is recorded in the public Rekor transparency log. Sigstore proves who signed an artifact, not whether it is safe.

Sigstore is a set of public services plus the clients that talk to them: Fulcio issues certificates, Rekor logs signatures, and a trust root distributed through The Update Framework (TUF) tells verifiers which keys to trust. The clients include cosign for container images, sigstore-python, and sigstore-js, which the npm CLI uses. The mode most people mean is "keyless signing": no private key outlives the signing operation. npm's provenance attestations, PyPI's digital attestations, Maven Central's Sigstore signature validation, and CPython's Sigstore-only release signing all run on it.

Sigstore fixed the problem that kept package signing from working: nobody could manage keys. It replaced that with a harder problem: deciding which identity you expect. A valid Sigstore signature only means some OIDC identity signed these bytes at a logged time. In the keyv and cacheable compromise on August 4, 2026, keyv@6.0.0 shipped malware with a passing provenance attestation, because the project's real release workflow built already-trojanized source. Our position: verify Sigstore signatures, pin the identity you expect, alert when it changes, and still read the code. Most tooling stops at the first step. The npm CLI checks that an attestation chains to Sigstore's root. It does not check which workflow signed it.

What is Sigstore?

#

Sigstore is free, public signing infrastructure for open source software. The Linux Foundation announced it with Red Hat, Google, and Purdue University on March 9, 2021. Fulcio and Rekor reached general availability on October 25, 2022, and Sigstore became an OpenSSF graduated project in March 2024.

To see why it exists, start with how code signing normally works. A publisher generates a key pair, publishes the public key, and protects the private key for as long as anyone might verify an old release. A verifier has to learn the right public key out of band, from a keyserver, a web of trust, or a README. If the private key leaks, the revocation has to reach every verifier before the attacker does. Package ecosystems struggled with every one of those steps, and in practice few maintainers signed and fewer users verified. When CPython dropped PGP signatures starting with Python 3.14, PEP 761 called long-lived release keys "an unnecessary burden in the new age of ergonomic and ephemeral signing keys."

Sigstore swaps out three parts of that model:

  • Identity instead of a key. The signer authenticates to an OpenID Connect (OIDC) provider: a Google or GitHub account for a person, or a CI system's workload identity for a pipeline.
  • A certificate that lives for minutes. Fulcio binds that identity to an ephemeral public key in an X.509 certificate valid for 10 minutes.
  • Transparency instead of revocation. Every certificate goes into a Certificate Transparency (CT) log and every signature into Rekor. A verifier checks that the signature was logged while the certificate was valid. A key that existed for seconds never needs revoking.

How does Sigstore signing work?

#

Sigstore signing works by trading an OIDC identity token for a short-lived certificate, signing once, and logging the result. The figure follows the case most developers now hit without noticing: a GitHub Actions job publishing an npm package with provenance. Four parties take part: the CI job running the npm CLI and sigstore-js, GitHub's OIDC token issuer, Fulcio with its CT log, and Rekor.

CI jobnpm / sigstore-js
GitHub OIDCtoken issuer
FulcioCA + CT log
Rekortransparency log
CI job1Generate ephemeral P-256 key pair in memory
CI job → GitHub OIDC2Request ID token, aud=sigstore
GitHub OIDC → CI job3Signed JWT: repo, workflow ref, commit, event
CI job → Fulcio4JWT + public key + proof of possession
Fulcio5Verify JWT, copy claims into extensions, log precert in CT
Fulcio → CI job6Leaf cert, valid 10 min, SAN = release.yml@refs/heads/main
CI job7Sign DSSE envelope over tarball SHA-512
CI job → Rekor8Upload signature + cert + digest
Rekor → CI job9logIndex, integratedTime (inside the 10 min), signed inclusion promise
CI job10Discard private key, write bundle
Keyless signing for one npm provenance attestation. No private key survives step 10.

The figure's ten steps, in order:

  1. The CI job generates an ephemeral ECDSA P-256 key pair in memory.
  2. It asks GitHub's OIDC provider for an identity token with the audience sigstore.
  3. GitHub returns a signed JWT whose claims name the repository, workflow file and ref, commit, and triggering event.
  4. The job sends the JWT, the public key, and proof that it holds the private key to Fulcio.
  5. Fulcio verifies the JWT against GitHub's published keys, copies the claims into certificate extensions, and logs a precertificate in a CT log.
  6. Fulcio returns a leaf certificate valid for 10 minutes, with the CT log's receipt embedded. Its subject alternative name (SAN) is the workflow, release.yml@refs/heads/main.
  7. The job signs a DSSE envelope around an in-toto statement that names the tarball's SHA-512 digest.
  8. It uploads the signature, certificate, and digest to Rekor.
  9. Rekor returns a log index, an integrated time that falls inside the certificate's 10 minutes, and a signed inclusion promise.
  10. The job discards the private key and packages the certificate, signature, and log receipts into a Sigstore bundle.

Verification takes five:

  1. Load the trusted root through TUF: Fulcio's CA certificates and the public keys of Rekor and the CT logs.
  2. Chain the leaf certificate to Fulcio's root and check the CT receipt.
  3. Verify the Rekor receipt and check that the integrated time falls inside the certificate's validity window.
  4. Verify the signature over the artifact's digest.
  5. Compare the certificate's identity with what you expected: the SAN, the OIDC issuer, and any extensions you care about.

Steps 1 through 4 are mechanical and work the same way for everyone. Step 5 is policy, and it is the only step that tells you anything about who published the artifact.

Inside a real Sigstore bundle

#

A Sigstore bundle is the JSON object that carries everything an offline verifier needs. You can fetch real ones from the npm registry. We pulled the attestations for @tanstack/react-router@1.170.46, published from GitHub Actions on October 22, 2026. Earlier versions of this package were hit in the TanStack compromise in May 2026, which matters later. This release is clean:

Bash
curl -s 'https://registry.npmjs.org/-/npm/v1/attestations/@tanstack%2freact-router@1.170.46' | node -e '
let s=""; process.stdin.on("data", d => s += d).on("end", () => {
  for (const a of JSON.parse(s).attestations) {
    const vm = a.bundle.verificationMaterial
    console.log(a.predicateType)
    console.log("  bundle:", a.bundle.mediaType)
    console.log("  signed with:", vm.certificate ? "Fulcio certificate" : "npm key " + vm.publicKey.hint)
    console.log("  rekor logIndex:", vm.tlogEntries[0].logIndex, "integratedTime:", vm.tlogEntries[0].integratedTime)
  }
})'
Plain text
https://github.com/npm/attestation/tree/main/specs/publish/v0.1
  bundle: application/vnd.dev.sigstore.bundle+json;version=0.2
  signed with: npm key SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U
  rekor logIndex: 3399297868 integratedTime: 1792699819
https://slsa.dev/provenance/v1
  bundle: application/vnd.dev.sigstore.bundle.v0.3+json
  signed with: Fulcio certificate
  rekor logIndex: 3399272419 integratedTime: 1792699754

Every npm version published with package provenance carries two attestations. The SLSA provenance attestation is signed keylessly in the CI job with a Fulcio certificate. The publish attestation is signed by the registry with npm's own long-lived key, the one listed at registry.npmjs.org/-/npm/v1/keys, and records that npm accepted this exact tarball. Both are logged in Rekor. Here the registry's entry landed 65 seconds after the CI job's. The provenance entry's global log index, 3,399,272,419, gives a sense of the public log's scale.

The Fulcio certificate is the interesting one. Decoded with openssl x509 -inform der -text, trimmed:

Plain text
Issuer: O = sigstore.dev, CN = sigstore-intermediate
Validity
    Not Before: Oct 22 20:09:14 2026 GMT
    Not After : Oct 22 20:19:14 2026 GMT
Subject:
X509v3 Extended Key Usage: Code Signing
X509v3 Subject Alternative Name: critical
    URI:https://github.com/TanStack/router/.github/workflows/release.yml@refs/heads/main
1.3.6.1.4.1.57264.1.8:  https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.11: github-hosted
1.3.6.1.4.1.57264.1.12: https://github.com/TanStack/router
1.3.6.1.4.1.57264.1.13: 6b9fe6d29552270836c511bc9b343d73541f4424
1.3.6.1.4.1.57264.1.15: 165670309
1.3.6.1.4.1.57264.1.17: 72518640
1.3.6.1.4.1.57264.1.20: push
1.3.6.1.4.1.57264.1.21: https://github.com/TanStack/router/actions/runs/39845830622/attempts/1
1.3.6.1.4.1.57264.1.22: public
CT Precertificate SCTs: ...

Four details matter.

The Subject field is empty. A Fulcio certificate has no name in the traditional sense. The identity is the SAN, and for a CI job the SAN is a workflow file at a ref, not a person.

The validity window is exactly 10 minutes. Rekor's integrated time for the provenance entry, Unix 1792699754, is 20:09:14 UTC, the same second as Not Before. The signature was logged inside the window, so it stays verifiable long after the certificate expires.

The extensions carry the build context. Fulcio documents each OID under 1.3.6.1.4.1.57264.1:

OIDNameValue in this certificate
1.8Issuerhttps://token.actions.githubusercontent.com
1.12Source Repository URIhttps://github.com/TanStack/router
1.13Source Repository Digest6b9fe6d2...41f4424 (the commit)
1.15Source Repository Identifier165670309 (GitHub's immutable repository ID)
1.17Source Repository Owner Identifier72518640
1.20Build Triggerpush
1.21Run Invocation URI.../actions/runs/39845830622/attempts/1
1.23Deployment Environmentabsent

The missing 1.23 extension indicates the signing job did not run in a GitHub deployment environment. A policy could require one, and this certificate would fail it.

The DSSE payload is an in-toto statement. Its subject is the package URL plus the tarball's SHA-512, and that digest is byte-for-byte the registry's dist.integrity value decoded from base64:

JSON
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{
    "name": "pkg:npm/%40tanstack/react-router@1.170.46",
    "digest": { "sha512": "7b3d6234886c3e31a56831e0dc15d6b3aec4595218cc1dad6e8a441d80c449d9..." }
  }],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "externalParameters": { "workflow": {
        "repository": "https://github.com/TanStack/router",
        "path": ".github/workflows/release.yml", "ref": "refs/heads/main" } },
      "resolvedDependencies": [{ "uri": "git+https://github.com/TanStack/router@refs/heads/main",
        "digest": { "gitCommit": "6b9fe6d29552270836c511bc9b343d73541f4424" } }]
    },
    "runDetails": { "builder": { "id": "https://github.com/actions/runner/github-hosted" } }
  }
}

We then verified the bundle offline with the same @sigstore/verify library npm ships, three times: with no identity policy, with the expected identity, and with a lookalike repository.

JavaScript
const M = '/opt/node22/lib/node_modules/npm/node_modules/'
const { toTrustMaterial, Verifier, toSignedEntity } = require(M + '@sigstore/verify')
const { bundleFromJSON } = require(M + '@sigstore/bundle')
const { TrustedRoot } = require(M + '@sigstore/protobuf-specs')
const fs = require('fs')

// Trusted root shipped with npm as a TUF seed (the sandbox could not reach the TUF CDN)
const seed = JSON.parse(fs.readFileSync(M + '@sigstore/tuf/seeds.json'))['https://tuf-repo-cdn.sigstore.dev']
const root = JSON.parse(Buffer.from(seed.targets['trusted_root.json'], 'base64'))
const verifier = new Verifier(toTrustMaterial(TrustedRoot.fromJSON(root)),
  { tlogThreshold: 1, ctlogThreshold: 1, tsaThreshold: 0 })

const att = JSON.parse(fs.readFileSync(process.argv[2]))
const entity = toSignedEntity(bundleFromJSON(
  att.attestations.find(a => a.predicateType === 'https://slsa.dev/provenance/v1').bundle))
const issuer = 'https://token.actions.githubusercontent.com'

for (const san of [
  undefined,
  'https://github.com/TanStack/router/.github/workflows/release.yml@refs/heads/main',
  'https://github.com/attacker/router/.github/workflows/release.yml@refs/heads/main',
]) {
  try {
    verifier.verify(entity, san ? { subjectAlternativeName: san, extensions: { issuer } } : undefined)
    console.log('PASS', san ?? '(no policy)')
  } catch (e) { console.log('FAIL', san, '->', e.code) }
}
Plain text
PASS (no policy)
PASS https://github.com/TanStack/router/.github/workflows/release.yml@refs/heads/main
FAIL https://github.com/attacker/router/.github/workflows/release.yml@refs/heads/main -> UNTRUSTED_SIGNER_ERROR

The first line is the one to remember. With no policy, verification succeeds for any identity Fulcio has ever certified. Our sandbox also showed the TUF dependency in practice: npm audit signatures failed with "Failed to download" because it could not reach Sigstore's TUF repository, so the script uses the trusted root npm bundles as a seed.

Sigstore trusted rootFulcio CA certs, Rekor and CT log keys, delivered by TUF
Pins which CA and logs you trust. Root rotation needs TUF.
Fulcio leaf certificateSAN …/TanStack/router/.github/workflows/release.yml@refs/heads/main, issuer GitHub Actions, valid 20:09:14 to 20:19:14 UTC
Binds an OIDC identity to a 10-minute public key. CT receipt proves Fulcio logged it.
Rekor entrylogIndex 3399272419, integratedTime 20:09:14 UTC
Proves the signature existed while the cert was valid, in public, forever.
DSSE signature → in-toto statementsubject pkg:npm/%40tanstack/react-router@1.170.46, workflow, commit 6b9fe6d
Binds the build claims to the identity's key.
TarballSHA-512 7b3d6234… = registry dist.integrity
Binds the statement to the exact bytes you install.
Your identity policyExpected SAN and issuer, checked against the leaf certificate
The only layer that says who you trust. npm's client skips it.
Not covered by any layerWhat the source contains, what the build pulled from caches or dependencies, whether a human meant to release it
Where Ultralytics, TanStack, and keyv lived.
What each layer of a Sigstore bundle binds, using the real values from @tanstack/react-router@1.170.46.

The figure stacks the layers of this bundle from the trust root down to the tarball. The Sigstore trusted root, delivered by TUF, pins the Fulcio CA certificates and the Rekor and CT log keys. The Fulcio leaf certificate binds the identity, SAN https://github.com/TanStack/router/.github/workflows/release.yml@refs/heads/main with the GitHub Actions issuer, to a public key valid from 20:09:14 to 20:19:14 UTC, and its CT receipt proves Fulcio logged it. The Rekor entry, log index 3399272419 at integrated time 20:09:14 UTC, proves the signature existed while the certificate was valid. The DSSE signature binds the in-toto statement, subject pkg:npm/%40tanstack/react-router@1.170.46 built from commit 6b9fe6d, to that key. The tarball's SHA-512, beginning 7b3d6234, equals the registry's dist.integrity. Beside the chain sits your identity policy, the expected SAN and issuer, which is the only layer that says who you trust and the one npm's client skips. Below everything is what no layer covers: what the source contains, what the build pulled from caches or dependencies, and whether a human meant to release it.

What does a Sigstore signature prove?

#

A Sigstore signature proves that a specific OIDC identity signed specific bytes at a publicly logged time. For a CI signature, that identity is a workflow file at a ref in a repository, running on a given commit and trigger. The proof is strong: forging it requires compromising Fulcio, the OIDC provider, or the transparency logs, and the logs make that kind of compromise visible.

A Sigstore signature does not prove the code is safe. It does not prove the source was what the maintainers intended, that the build's inputs were clean, or that a human approved the release. Socket said this when npm launched provenance in April 2023: npm's provenance is "not about what something is." Three incidents since then show where the boundary sits.

  • Ultralytics, December 2024. In the Ultralytics compromise, attackers poisoned the project's GitHub Actions cache, and the release workflow built and published the first two of four malicious versions to PyPI with valid attestations.
  • TanStack, May 11, 2026. The TanStack compromise, part of the Mini Shai-Hulud campaign, ran attacker code that read an OIDC token out of the runner's memory and published 84 versions across 42 packages through the project's trusted publisher. TanStack's postmortem puts it plainly: "OIDC trusted-publisher binding has no per-publish review." Our analysis of the payload indicates it also submits a Sigstore provenance attestation after publishing.
  • keyv, August 4, 2026. In the keyv and cacheable compromise, keyv@6.0.0 carried a passing attestation because the legitimate release workflow built trojanized source. The payload also contained a provenance module that requests Fulcio certificates and submits Rekor entries, so the worm could ship fresh, verifiable provenance on every package it republished.

In all three, Sigstore did what it was built to do. Where attestations existed, they named the real repository, the real workflow, and the real commit, because the attacker was inside the pipeline that holds that identity. The full CI chain is covered under GitHub Actions security and npm trusted publishing.

What caught these releases was looking inside the tarball. Socket's AI scanner flagged all 84 TanStack versions within six minutes of publication, from what changed in the package: a new 2.3 MB obfuscated router_init.js and a git dependency with a prepare hook. The provenance check passed. The contents check did not.

Sigstore still earned its keep in these incidents. Rekor is append-only, so a malicious release that carried provenance leaves a permanent, timestamped record of the workflow run that produced it. In Ultralytics, the attestation timestamps helped researchers pin down the cache poisoning. Provenance is necessary. It is not sufficient.

Why does Sigstore verification need an identity policy?

#

Sigstore verification without an identity policy accepts any signer Fulcio will certify, and Fulcio certifies any account at a supported OIDC provider. Any GitHub account can run a workflow that obtains a valid certificate for its own repository. A typosquatting package published from an attacker's repository can carry provenance that verifies perfectly. The signature tells you the package came from github.com/attacker/router. It does not tell you that github.com/attacker/router is not the project you meant.

npm's registry closes part of that gap at publish time by checking that the provenance repository matches the repository field in package.json. The client does not check identity at all. This is the verification call in pacote 22.0.1, the fetcher used by npm 12.2.1, the latest npm release as of November 2026:

JavaScript
// Verify the statement subject matches the package, version
if (subject.name !== purl) { throw ... }
// Verify the statement subject matches the tarball integrity
if (subject.sha512 !== integrityHexDigest) { throw ... }

const options = {
  tufCachePath: this.tufCache,
  tufForceCache: true,
  keySelector: publicKey ? () => publicKey.pemkey : undefined,
}
await sigstore.verify(bundle, options)

The options carry no certificateIdentityURI and no certificateIssuer. npm confirms the name, the digest, and the chain to Sigstore's root, and accepts whatever identity signed. That check runs only under npm audit signatures. In npm 12.2.1, npm install does not verify attestations at all.

Real identity policy names the signer you expect. For CPython, python.org publishes one identity per release line: hugo@python.org with issuer https://github.com/login/oauth for 3.14 and 3.15. Verification then looks like this:

Bash
python -m sigstore verify identity \
  --bundle Python-3.14.0.tgz.sigstore \
  --cert-identity hugo@python.org \
  --cert-oidc-issuer https://github.com/login/oauth \
  Python-3.14.0.tgz

That works for a project with one release manager and a published table. It does not scale to a dependency tree of thousands of packages, and nobody publishes an expected identity for each of them. The practical substitute is continuity: remember who signed the last release and alert when that changes. pnpm's trustPolicy: no-downgrade does a version of this. It refuses to install a package whose trust evidence dropped, for example from trusted publisher to provenance-only or to nothing. That catches the pattern behind many a maintainer account takeover: a package that always shipped from CI suddenly ships from a stolen token.

Continuity has a blind spot, and the incidents above sit in it. When the attacker runs inside the pinned workflow, the new certificate matches the old one in every field except the commit and run identifiers. Identity policy stops impostors. It cannot stop the pipeline from signing malware.

Sigstore vs PGP and long-lived keys

#

Sigstore competes with two older models: PGP, which Maven Central and many Linux distributions still use, and long-lived key pairs held in a KMS, which cosign also supports.

Sigstore keylessLong-lived key in a KMS (cosign)PGP
What the signer holdsNothing after signing; the key lives for one operationA private key, usually in cloud KMS or an HSMA private key, often on a laptop, for years
What identity meansOIDC identity: an email address or a CI workflow at a refWhoever controls the keyA key fingerprint, linked to a person by the web of trust
Compromise recoveryNothing to revoke; check Rekor for entries under your identityRotate the key and tell every verifierRevocation certificate, which verifiers rarely fetch
Public recordEvery signature in Rekor, every certificate in a CT logOptionalNone
Offline or air-gapped verificationWorks with a cached trusted root and the bundleWorks with the public keyWorks with the public key
PrivacySigner identity is published permanently in a public logPrivateThe key is public; the signing history is not
Main failure modeCompromised CI or OIDC account signs with the real identityLeaked key signs anything, silentlyLeaked key signs anything, and verification is rare in practice

The verdict: use Sigstore keyless signing for open source artifacts built in CI. Its signatures are auditable, there is nothing to leak, and npm, PyPI, and Maven Central all accept them. Use a KMS-held key when signing must stay inside a private network, when the signer's identity must not appear in a public log, or when verifiers cannot depend on Sigstore's public infrastructure. Keep PGP only where a downstream consumer requires it, and plan to drop it, as CPython did.

Where Sigstore runs today

#

Sigstore is the signing layer most open source registries are building on, though verification is still opt-in for consumers almost everywhere.

  • npm. npm launched provenance in April 2023. Since npm's trusted publishing launch on July 31, 2025, the CLI generates provenance automatically for trusted publishes from public repositories. Adoption among the packages that matter most was low for years: in February 2025, vlt's reproduce research found only 3.72% of 5,000 high-impact npm packages had provenance attestations.
  • PyPI. PyPI's digital attestations, defined in PEP 740, reached general availability on November 14, 2024, with more than 20,000 attestations already uploaded. Projects that publish through a trusted publisher with PyPA's GitHub publish action get them by default.
  • Maven Central. Maven Central began validating Sigstore signatures in early 2025. They are optional there, and PGP remains the required standard.
  • CPython. Python 3.14 is the first CPython release line signed with Sigstore only, under PEP 761.
  • Containers. cosign signs OCI images and stores signatures alongside them in the registry.

The infrastructure is still changing. Rekor v2 reached general availability on October 10, 2025, with a tile-based log that is cheaper to run. It supports two entry types, hashedrekord and dsse, and drops the search index, so you can no longer ask the public log "show me every entry for this artifact." Rekor v1 keeps running, and its freeze will be announced a year in advance.

The trade-offs are real. Every keyless signature with a personal account writes an email address into a public, permanent log. Verifiers depend on Sigstore's TUF repository for root rotation. And a CI identity is only as trustworthy as every step that runs in that job.

Signing is solved; intent is not

#

Sigstore answers "which pipeline produced these bytes?" with more rigor than any system before it, and it does so for free. It cannot answer "did the maintainers mean to ship this?" or "is it safe to run?" An OIDC token carries no notion of which step in a job requested it, so a credential read out of runner memory produces a certificate with the same identity as the release step. Until CI providers can bind identity to a single step, and until package managers verify an expected identity by default, treat a valid Sigstore signature as an address on an envelope. It tells you where the package came from. You still have to open it.