aud=sigstorerelease.yml@refs/heads/mainSigstore 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.
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:
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.
aud=sigstorerelease.yml@refs/heads/mainThe figure's ten steps, in order:
sigstore.release.yml@refs/heads/main.Verification takes five:
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.
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:
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)
}
})'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: 1792699754Every 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:
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:
| 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:
{
"_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.
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) }
}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_ERRORThe 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.
ā¦/TanStack/router/.github/workflows/release.yml@refs/heads/main, issuer GitHub Actions, valid 20:09:14 to 20:19:14 UTCpkg:npm/%40tanstack/react-router@1.170.46, workflow, commit 6b9fe6d7b3d6234⦠= registry dist.integrityThe 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.
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.
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.
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:
// 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:
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.tgzThat 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 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.
Sigstore is the signing layer most open source registries are building on, though verification is still opt-in for consumers almost everywhere.
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.
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.
Manifest confusion is a mismatch between an npm package's registry manifest and the package.json in its tarball, two copies that tools trust inconsistently.
A software bill of materials (SBOM) is a machine-readable inventory of the components, versions, and dependency relationships inside a piece of software.
Software composition analysis (SCA) inventories the third-party code in an application and checks each component for vulnerabilities, licenses, and malware.