Refolk
ReferenceEngineering and open source

The Package Provenance Reference: Artifact to Repo, Ref, and Commit

For any package on seven registries, fetch its provenance artifact, verify it with identity pinned, and state the commit it binds and the failure mode it leaves open.

19 min readLast reviewed November 12, 2026Read as Markdown

Key takeaways

  • gh attestation verify with only -R accepts an attestation made by any workflow in the repository on any ref, so a defensible check also pins --signer-workflow and --source-ref and adds --deny-self-hosted-runners.
  • Valid SLSA provenance did not stop the TanStack compromise: 84 malicious versions across 42 @tanstack npm packages carried valid provenance because the attacker coerced the legitimate release pipeline.
  • Provenance names a commit, not a tarball's contents: in one measurement of npm tarballs against their attested commits, 60% of files existed only in the tarball.
  • crates.io and NuGet publish no provenance artifact at all, so the correct reading there is 'none possible', not 'missing'.
  • Low adoption figures partly measure release cadence: only 3.72% of 5,000 high-impact npm packages had provenance, while PyPI's top-360 adoption rose 309% once trusted publishing made attestations the default.
  • As of npm 12.2.1, npm install never verifies attestations, so provenance is checked only when a reviewer or CI step runs a verifier on purpose.

I keep this reference open during every dependency review, whether I'm looking at a new dependency or a new release of one. It covers each provenance artifact on npm, PyPI, RubyGems, Maven Central, NuGet, crates.io, and container registries: where it lives, how to verify it, which fields tie it to a repository, ref, commit, and workflow, and how it misleads. It is for engineering managers, technical founders, developer-relations leads, and technical sourcers who sign off on what ships. Every row ends in a one-line reading: what the artifact proves, and which named failure mode it leaves open.

Four terms first:

  • Provenance is a signed statement recording where and how an artifact was built.
  • An attestation is any signed claim bound to an artifact's digest, meaning its cryptographic hash.
  • Trusted publishing lets a CI workflow publish to a registry with a short-lived OIDC identity token instead of a stored API token.
  • Sigstore is the public signing system whose certificate authority, Fulcio, certifies identities such as a CI workflow.

What each provenance artifact proves

Each artifact proves one narrow claim. A registry signature proves the registry served these bytes. A publish attestation proves which identity uploaded them. SLSA build provenance proves which repository, commit, and workflow built them. None of them proves the code is safe to run.

ArtifactWhereSigned byProves
Registry signaturenpm, NuGetRegistry keyRegistry served these bytes
npm publish attestationnpmnpm registry keynpm accepted this publish
SLSA v1 provenancenpm, GitHub, OCISigstore certificateRepo, commit, workflow built this digest
PEP 740 publish attestationPyPITrusted Publisher identityThat publisher uploaded this file
Sigstore bundleRubyGemsSigstore certificateA Sigstore identity signed this gem
PGP signatureMaven CentralMaintainer keyA key holder signed it

Read the table as a set of separate claims, not a ranking. A package can carry a flawless SLSA statement and still be malicious, because the statement describes the build, not what the code does.

Two artifacts are left out of the table because my sources do not pin them down:

  • Maven Central .sigstore.json. This file is optional and published alongside the required PGP signature. I have not sourced its per-file URL format, so find it in the artifact's directory listing rather than building the path yourself.
  • Rebuild attestations. An independent rebuild that reproduces the published bytes is the only check here that speaks to contents. In vlt's study of 5,000 high-impact npm packages, 5.78% reproduced. My sources don't establish which service signs rebuild attestations for each ecosystem, so confirm that in the registry's documentation before you count one.

The registry matrix

RegistrySignatureProvenanceAttestation checked at install
npmRegistry ECDSASLSA + publishNo
PyPIPGP removedPEP 740 publishNo
RubyGemsOptional X.509SigstoreNo
Maven CentralPGP requiredOptional .sigstore.jsonNo
NuGetRepository + authorNonen/a
crates.ioNoneNonen/a

The last column surprises people. None of these registries' install clients checks attestations at install time. Here is what each client does by default:

  • npm: as of npm 12.2.1, npm install never verifies attestations.
  • pip and uv: install-time checks are tracked in open issues and have not shipped.
  • RubyGems: signature checks are opt-in with -P, and attestations are not checked at all.
  • NuGet: signatures are checked by default on Windows and Linux.
  • Maven: signature checks need a plugin.
  • crates.io: Cargo checks only the index checksum.

So provenance gets checked only when a reviewer or a CI step runs a verifier on purpose.

Where each registry serves its artifact, and what verifies it

npm, PyPI, and RubyGems serve per-version artifacts from fixed endpoints you can fetch with curl. GitHub artifact attestations and container images are checked with gh attestation verify or cosign. crates.io and NuGet have no provenance artifact to fetch.

RegistryFetch fromVerify with
npm/-/npm/v1/attestations/<pkg>@<ver>npm audit signatures
PyPI/integrity/<project>/<ver>/<file>/provenancepypi-attestations verify
RubyGems/api/v1/attestations/<name>-<ver>.jsonSigstore bundle verifier
Maven CentralPGP signature, optional .sigstore.jsonMaven signature plugin
OCI imageoci://<image-uri>gh attestation verify, cosign 3.0+
crates.io, NuGetNothing to fetchRecord "none possible"

npm

  • Registry signatures: ECDSA, stored in each version's dist.signatures, with public keys at /-/npm/v1/keys. The signed message is ${package.name}@${package.version}:${package.dist.integrity}, so a valid signature binds name, version, and integrity hash together. These signatures replaced npm's old PGP signatures.
  • Provenance: every version published with provenance has two bundles at the attestations endpoint. One is a SLSA v1 provenance statement signed with a Sigstore certificate. The other is an npm publish attestation signed with a registry key.
  • Defaults: provenance started as an opt-in --provenance flag. Since trusted publishing became generally available, packages published that way get provenance by default.
  • Client: verification needs npm CLI v9.5.0 or later.

PyPI

  • PyPI stopped accepting PGP signatures. PEP 740 attestations are served per distribution file from the PyPI Integrity API.
  • PyPI only accepts attestations with a verifiable signature at upload, and the signing identities are Trusted Publisher identities.
  • PEP 740 allows an attestation to be added to a file after upload, so a file with no attestation at review time may have one later. Record when you looked.
  • Trail of Bits' "Are we PEP 740 yet?" tracker shows adoption across the most-downloaded projects.

RubyGems

  • Sigstore bundles are served at /api/v1/attestations/<name>-<version>.json. The Gems.attestations method in the rubygems/gems client fetches them.
  • Automatic attestation in gem push ships in the RubyGems 4.1 line. If signing fails, the gem is pushed anyway, without an attestation.
  • Gems can also carry optional X.509 signatures, which are only checked with -P.

Maven Central

  • PGP signatures are required. A .sigstore.json file is optional.
  • PGP binds an artifact to a key, not to a repository or a build. Ownership of the group ID is a separate control, which Maven Central requires you to prove through domain verification.
  • In practice, Maven Central's PGP signature and its Sigstore file answer different questions. A PGP pass says a key holder signed the artifact, and the namespace check says the publisher controls the domain. Neither names a commit. If you need a build identity, look for the optional Sigstore file and read the identity its certificate names.

NuGet and crates.io

  • NuGet: repository signatures plus optional author signatures, checked by default on Windows and Linux. There is no provenance artifact. The reading is "none possible".
  • crates.io: the index records a SHA-256 of each .crate, but the index itself is unsigned. Trusted publishing exists for GitHub Actions and GitLab, but it produces no attestation, and Cargo's RFC 3403 for attestations is still open. The reading is "none possible".

Containers and GitHub artifact attestations

  • gh attestation verify accepts oci://<image-uri> and enforces the https://slsa.dev/provenance/v1 predicate type by default.
  • Image attestations are also pushed to Docker Hub, where cosign 3.0 or newer can verify them. Older cosign versions report no signatures, so a client limitation looks like a missing attestation.
  • Attestations from private repositories use GitHub's own Sigstore instance with no transparency log, so there is no public record to cross-check.

Trusted publishing vs provenance attestation

Trusted publishing controls who may upload. A provenance attestation is a public record of what built the upload. A registry can enforce the first without publishing the second, and crates.io does exactly that.

On each registry, the two relate like this:

  • npm: publishing through trusted publishing generates provenance by default.
  • PyPI: attestations are the default when you use Trusted Publishing with the canonical publish action.
  • crates.io: trusted publishing exists, but there are no attestations. My sources don't show any public per-version record of which workflow published a given version. After the fact, you cannot tell from the registry whether a crate version came through trusted publishing or a personal token.

The two sets don't line up neatly even on npm. When Socket checked the latest release of 30 widely used npm packages, 13 carried provenance and 14 were published through a trusted publisher. Check each separately and never infer one from the other.

The default matters more than the feature. Provenance becomes common only where trusted publishing switches it on. PyPI's top-360 adoption grew 309% in about eight months, while opt-in npm provenance sits between 3.72% and 21.5% depending on the sample (see the adoption table below).

Which fields bind a package to its repo, ref, commit, and workflow

In SLSA v1 provenance, the binding fields name the source repository, the commit SHA, and the workflow file and job that ran, all tied to the artifact's digest. Read them from the signed statement itself, never from a registry web page.

An npm provenance statement binds the tarball digest to the GitHub repository URL, the git commit SHA, and the exact workflow file and job. In a real npm payload, predicateType is https://slsa.dev/provenance/v1, and the fields repository and invocationId are present. I have not verified the full field paths for ref, commit, and workflow path, so I don't publish a one-line extraction script. Decode the statement, find the fields by eye the first few times, and then script against what you actually saw.

gh attestation verify maps each binding to a flag. The catch is the third column:

Bindinggh flagChecked by default
Repository-RYes, as passed
Workflow--signer-workflowNo
Ref--source-ref (gh 2.68.0+)No
Commit--source-digestNo
Runner--deny-self-hosted-runnersNo

What each verification layer proves

  1. Content review
    Tarball diffed against the commit, plus content scanning; the only layer about behaviour
  2. Identity pins
    Your expected repo, workflow, ref, and runner, enforced by you at verify time
  3. Build provenance
    SLSA v1 statement naming repo, commit, workflow, and run for this digest
  4. Publish attestation
    Which trusted publisher or registry pipeline uploaded the file
  5. Registry signature
    The registry vouches for this name, version, and integrity hash
Each layer proves only its own claim, so a pass at a lower layer says nothing about the layers above it.

How provenance misleads: the failure modes

Provenance misleads in two ways. Either the verifier accepts more identities than you meant, or the right identity built something you would never have approved. These seven documented modes are the ones I check for by name.

1. Repo-only verification

gh attestation verify with only -R accepts an attestation made by any workflow in the repository, on any ref. False positive: a pull-request or branch build passes as if it were the release. Check: require --signer-workflow, --source-ref, and --deny-self-hosted-runners.

2. Workflow pinned, ref not

--signer-workflow matches the workflow's path at any ref. False positive: the release workflow file, run from a feature branch, passes. Check: add --source-ref for the tag or branch you expect.

3. HEAD link instead of the built ref

PyPI's UI has shown a link to the workflow file that points at HEAD, the current tip of the default branch, not the ref that built the file. False positive: you review today's workflow and approve an artifact built by an older one. Check: read the commit from the attestation itself, then open the workflow at that commit.

4. Coerced legitimate pipeline

In the TanStack compromise, an attacker chained pull_request_target abuse, cache poisoning, and OIDC token theft to publish 84 malicious versions across 42 official @tanstack npm packages (CVE-2026-45321, advisory GHSA-g7cv-rxg3-hmpx). Those versions carried valid SLSA provenance. The malicious code never appeared in the source tree, because it was fetched at install time from a fork. Provenance confirms which pipeline produced the artifact, not whether that pipeline behaved as intended. A StepSecurity researcher opened TanStack/router#7383 about 26 minutes after the first malicious publish. No published source shows a provenance-only check that would have caught it. Check: diff the tarball against the attested commit, and apply a cooldown before adopting new versions.

When a reading like this is ambiguous, the fastest resolution is often a person: the maintainer who owns the release workflow, or a supply-chain engineer who has taken apart a similar attack. Refolk is how I find both by describing them in plain English, instead of guessing at keyword strings.

5. Signer identity unchecked

npm audit signatures reports a pass that is valid for any identity Fulcio ever certified. False positive: an attestation from a stranger's repository passes the audit. Check: open the bundle and compare the repository and workflow against the expected values by hand.

6. Silent absence

Absence often comes back as a quiet "no attestation" instead of an error:

  • RubyGems: if signing fails, gem push falls back to an unattested push.
  • npm: my sources disagree on whether a missing attestation fails npm audit signatures. npm's documentation says missing or invalid attestations return an error. A practitioner source says the command exits non-zero only when a signature is invalid. Test it locally with a version you know has no provenance.
  • Containers: cosign older than 3.0 reports no signatures on Docker Hub images that have them.

Check: record pass, fail, and missing as three separate outcomes, and treat "missing" as a finding to explain.

7. Truthful provenance on hostile packages

Self-spreading npm worms have published packages that also carry valid provenance attestations. Typosquats work the same way: valid provenance only proves the attacker's own repository built them. Check: pin the expected repository. A valid attestation from the wrong repository is evidence against the package.

Provenance names the pipeline that built the artifact. It cannot tell you whether that pipeline was behaving.

Identity pinning against content checking

Tarball diffed against commitContents unchecked
Right code, unknown builder
Add workflow and ref pins before trusting the trace
Defensible reading
Record repo, ref, and commit, then pass to the adoption decision
README-grade verified
Treat as no provenance reading at all
Right builder, unseen contents
Diff the tarball and apply a cooldown
Repo onlyRepo, workflow, and ref pinned
A defensible provenance reading needs both the identity pinned and the contents checked, and most "verified" claims have neither.

Procedure: trace a published package to its repo and commit

The procedure takes a package version through seven steps: confirm provenance can exist, fetch it, verify it, pin the identity, read off the bindings, compare with the previous version, and inspect content separately. Steps 1 to 5 take a reviewer a few minutes per package. Run in CI, the verifier adds 5 to 15 seconds for large dependency trees.

Trace a package version to its source

  1. Identify the registry and its artifact type
    Use the registry matrix to learn whether provenance can exist. For crates.io and NuGet, record "none possible" and stop.
  2. Fetch the per-version artifact
    Curl the registry's per-version endpoint. Keep the JSON bundle, or record a 404 as an explicit absence.
  3. Run the documented verifier
    Use npm audit signatures, pypi-attestations verify, gh attestation verify, or cosign 3.0 or newer. Record pass, fail, and missing separately.
  4. Pin the identity
    Supply the expected repo, signer workflow, and source ref, and deny self-hosted runners. Where the tool has no pin flags, compare those fields by hand.
  5. Read off repo, ref, commit, and workflow run
    Take them from the decoded statement, not from a UI link. Review the workflow at that commit, not just the pass or fail status.
  6. Compare against the previous version
    Newly missing provenance, a changed repository, or a changed workflow is an anomaly to escalate.
  7. Inspect content separately
    Diff the tarball against the attested commit or run content scanning, because provenance says nothing about behaviour.

Step 4 is the one people skip, and it's where most false passes come from. Step 6 catches the most at the lowest cost: if a lockfile has provenance for axios@1.14.0 and none for axios@1.14.1, that gap alone is a strong anomaly signal. My sources disagree on where step 7 belongs. One puts content scanning after the signature audit and others run it in parallel. The order matters less than making sure it happens.

Fetch and verify, per registry
# npm: registry signatures and provenance for the installed tree (npm v9.5.0+)
npm audit signatures

# npm: both bundles for one version (SLSA provenance + publish attestation)
curl -s https://registry.npmjs.org/-/npm/v1/attestations/<package>@<version>

# PyPI: PEP 740 provenance for one distribution file
curl -s https://pypi.org/integrity/<project>/<version>/<filename>/provenance

# RubyGems: Sigstore bundle for one version
curl -s https://rubygems.org/api/v1/attestations/<name>-<version>.json

# GitHub artifact attestation or container image, fully pinned (gh 2.68.0+ for --source-ref)
gh attestation verify oci://<image-uri> \
  -R <owner>/<repo> \
  --signer-workflow <expected workflow path> \
  --source-ref <expected ref> \
  --deny-self-hosted-runners

Replace the angle-bracket values, and keep every pin on the gh line, because dropping one widens what passes.

The output of the procedure is one line per package version. The Publisher-Change Verdict and the Dependency Adoption Standard both consume this line. This reference only defines how to produce it.

One-line provenance reading
<package>@<version> | registry: <name> | artifact: <SLSA / PEP 740 / Sigstore bundle / PGP / none possible / missing / failed> | repo: <owner/repo> | ref: <ref> | commit: <sha> | workflow: <path>, run <invocationId> | pins: <repo, workflow, ref, runner> | vs previous: <same / changed / newly missing> | proves: <one clause> | does not rule out: <named failure mode>

Write one line per package version and paste it into the review ticket that feeds the adoption decision.

A finished "proves" clause reads like this: "built by the declared release workflow at the tagged commit in the declared repo; does not rule out a coerced pipeline."

Reading adoption numbers and absence

On its own, a missing attestation is weak evidence. Adoption is low, and attestations attach only to new releases. Absence becomes strong evidence when the previous version of the same package had provenance.

StudySampleShare with provenance
Socket30 latest releases43% (13 of 30)
Zennoxa484-package tree21.5%
Exaforce205 GitHub-published packages12.6%
dev.to auditTop 5012%
vlt5,000 packages3.72%
3.72%
High-impact npm packages with provenance in vlt's 5,000-package sample
Only 5.78% of the same sample reproduced from source, so on npm a missing attestation alone separates very little.

These studies used different samples, so they aren't a trend line. A small set of famous latest releases scores higher than a broad tree. Two more caveats apply:

  • Snapshots undercount. When PyPI attestations launched, about two-thirds of the most-downloaded packages hadn't released since attestations became available. A low share partly measures release cadence. PyPI later went from 21 to 86 of the top 360 packages with attestations.
  • Provenance names a commit, not a tarball. In Socket's comparison of published npm tarballs with their attested commits, 40% of files were identical and 60% existed only in the tarball. A clean provenance reading leaves that 60% unexamined, which is why step 7 exists.

Keeping this reference current, and who can read it with you

Defaults change faster than formats, so re-check the client and verifier behaviour each time you upgrade a tool. When a reading stays ambiguous, the people who can resolve it are few and concentrated in a handful of companies.

Re-check these items, which are the ones most likely to have moved:

  • Whether npm install verifies attestations in your npm version (as of 12.2.1 it does not).
  • Whether pip or uv has shipped the install-time checks tracked in open issues.
  • Whether your RubyGems version is in the 4.1 line, which attests automatically on gem push.
  • Whether Cargo RFC 3403 has landed, which would give crates.io a provenance row.
  • Which cosign and gh versions your CI pins (cosign 3.0+ for Docker Hub images, gh 2.68.0+ for --source-ref).
  • The live share on Trail of Bits' "Are we PEP 740 yet?" tracker, rather than any figure quoted here.
CountryProfilesChainguard in top sample
United States2477 of 25 sampled (28%)
Germany162 of 16 sampled (12.5%)
247
Profiles in the United States matching "software supply chain security" in Refolk's index
7 of the 25 sampled work at Chainguard, and the US pool is 15.4 times the 16 matching profiles in Germany.

In Refolk's index of professional profiles, the specialists who read these artifacts every day cluster at a few employers. Black Duck and Chainguard each account for 2 of Germany's top sample. When you need a reviewer or a second opinion, search for the job, not the jargon. A plain keyword query for "sigstore" in Refolk's index came back dominated by unrelated retail and "store" titles. Describing the role, as in "DevSecOps leads in the US who run SLSA provenance verification in CI", returns the people who actually do the work.

Before you hand the reading downstream

  • The registry row is recorded, including "none possible" for crates.io and NuGet.
  • The artifact was fetched from the per-version endpoint, or its 404 is recorded as missing.
  • Pass, fail, and missing are recorded as three separate outcomes.
  • The verification pinned repo, signer workflow, and source ref, and denied self-hosted runners where supported.
  • Repo, ref, commit, and workflow run were read from the signed statement, not from a UI link.
  • The reading was compared with the previous version's reading for the same package.
  • The tarball was diffed against the attested commit or sent through content scanning.
  • The one-line reading names at least one failure mode it does not rule out.

Questions practitioners ask

How do I verify npm provenance for a package I'm about to install?

Run npm audit signatures with npm CLI v9.5.0 or later, then fetch the version's bundles from the registry's /-/npm/v1/attestations/<package>@<version> endpoint and read the repository, commit, and workflow from the SLSA statement. The audit pass alone is valid for any identity Sigstore's certificate authority ever certified, so compare those fields by hand against the repository and release workflow you expect. npm install itself never verifies attestations as of npm 12.2.1.

What does a PyPI attestation prove?

A PEP 740 attestation proves that a specific distribution file was signed by a Trusted Publisher identity, and PyPI accepts only attestations with a verifiable signature. It does not prove the file's contents are safe. Its SLSA identity check is also looser than the publish attestation's: any identity under the project's repository is accepted. Attestations can be added after upload, so record the date you checked.

Is trusted publishing the same as a provenance attestation?

No. Trusted publishing decides who may upload, by letting a CI workflow authenticate with a short-lived token. A provenance attestation is a public, signed record of what built the upload. npm and PyPI generate attestations by default when you publish through trusted publishing. crates.io supports trusted publishing for GitHub Actions and GitLab but publishes no attestation and no established per-version record of the build source.

Does a Maven Central PGP signature prove which repository built the artifact?

No. A PGP signature binds the artifact to a key, so a pass proves that a key holder signed it, not that a particular repository, commit, or workflow built it. Namespace ownership is a separate control, proven through domain verification of the group ID. If you need a build identity, look for the optional .sigstore.json file and read the identity its certificate names.

Does a missing attestation make npm audit signatures fail?

My sources disagree, so test it locally before you rely on it. npm's documentation says missing or invalid signatures or attestations return an error, while a practitioner source reports a non-zero exit only for invalid signatures. Install a version you know has no provenance, run the audit, and record the exit code. Until then, track pass, fail, and missing as three separate outcomes.

Can a typosquatted or worm-published package have valid provenance?

Yes. Provenance only proves which repository and workflow built the package, so an attacker who builds from their own repository gets a truthful attestation naming that repository. Self-spreading npm worms have published packages carrying valid provenance attestations. The defence is to pin the expected repository and workflow during verification and compare against the previous version, not to treat any valid attestation as trust.

Try it on the search you came here for

Stop building boolean strings. Just describe the person.

Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.

  1. 01Describe them

    One plain sentence. Role, city, stack, stage, whatever matters to you.

  2. 02I read the web live

    GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.

  3. 03You read the shortlist

    Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.

  • No boolean, no filters, no seat to buy. One box.
  • Read at search time, so a profile updated yesterday counts today.
  • Every step visible as it runs, every name with its reason.

500 free credits on sign-up. No card, no demo call. See real searches.

Read next