# 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

Source: https://socket.dev/glossary/sigstore

Last updated: 2026-11-03

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](https://socket.dev/blog/npm-provenance), [PyPI's digital attestations](https://socket.dev/blog/pypi-introduces-digital-attestations), [Maven Central's Sigstore signature validation](https://socket.dev/blog/maven-central-adds-sigstore-signature-validation), and [CPython's Sigstore-only release signing](https://socket.dev/blog/python-3-14-released) 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](https://socket.dev/supply-chain-attacks/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](https://linuxfoundation.org/press-release/linux-foundation-announces-free-sigstore-signing-service-to-confirm-origin-and-authenticity-of-software) with Red Hat, Google, and Purdue University on March 9, 2021. Fulcio and Rekor [reached general availability](https://blog.sigstore.dev/sigstore-ga-ddd6ba67894d/) on October 25, 2022, and Sigstore [became an OpenSSF graduated project](https://blog.sigstore.dev/sigstore-openssf-graduation/) in March 2024.

To see why it exists, start with how [code signing](https://socket.dev/glossary/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](https://peps.python.org/pep-0761/) 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)](https://socket.dev/glossary/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](https://docs.sigstore.dev/fulcio/overview/).
- **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.

> Keyless signing for one npm provenance attestation. No private key survives step 10.
>
> Sigstore keyless signing sequence for an npm publish from GitHub Actions

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](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack) 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)
  }
})'
```

```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](https://socket.dev/glossary/package-provenance) carries two attestations. The [SLSA](https://socket.dev/glossary/supply-chain-levels-for-software-artifacts) 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:

```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](https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md) under `1.3.6.1.4.1.57264.1`:

| OID | Name | Value in this certificate |
| --- | --- | --- |
| 1.8 | Issuer | https://token.actions.githubusercontent.com |
| 1.12 | Source Repository URI | https://github.com/TanStack/router |
| 1.13 | Source Repository Digest | 6b9fe6d2...41f4424 (the commit) |
| 1.15 | Source Repository Identifier | 165670309 (GitHub's immutable repository ID) |
| 1.17 | Source Repository Owner Identifier | 72518640 |
| 1.20 | Build Trigger | push |
| 1.21 | Run Invocation URI | .../actions/runs/39845830622/attempts/1 |
| 1.23 | Deployment Environment | absent |

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) }
}
```

```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.

> What each layer of a Sigstore bundle binds, using the real values from @tanstack/react-router@1.170.46.
>
> What each layer of a Sigstore bundle binds, and what it leaves out

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](https://socket.dev/blog/ultralytics-pypi-package-compromised-through-github-actions-cache-poisoning), 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](https://socket.dev/supply-chain-attacks/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](https://tanstack.com/blog/npm-supply-chain-compromise-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](https://socket.dev/glossary/github-actions-security) and [npm trusted publishing](https://socket.dev/glossary/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](https://socket.dev/glossary/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](https://python.org/downloads/metadata/sigstore): `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`](https://pnpm.io/supply-chain-security) 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](https://socket.dev/glossary/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 keyless | Long-lived key in a KMS (cosign) | PGP |
| --- | --- | --- | --- |
| What the signer holds | Nothing after signing; the key lives for one operation | A private key, usually in cloud KMS or an HSM | A private key, often on a laptop, for years |
| What identity means | OIDC identity: an email address or a CI workflow at a ref | Whoever controls the key | A key fingerprint, linked to a person by the web of trust |
| Compromise recovery | Nothing to revoke; check Rekor for entries under your identity | Rotate the key and tell every verifier | Revocation certificate, which verifiers rarely fetch |
| Public record | Every signature in Rekor, every certificate in a CT log | Optional | None |
| Offline or air-gapped verification | Works with a cached trusted root and the bundle | Works with the public key | Works with the public key |
| Privacy | Signer identity is published permanently in a public log | Private | The key is public; the signing history is not |
| Main failure mode | Compromised CI or OIDC account signs with the real identity | Leaked key signs anything, silently | Leaked 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](https://socket.dev/blog/npm-trusted-publishing) 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](https://socket.dev/blog/vlt-launches-reproduce) 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](https://blog.sigstore.dev/pypi-attestations-ga/) 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](https://blog.sigstore.dev/rekor-v2-ga/) 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.
