Ecosystem and productionThe software supply chainPart 2
The Provenance Was Valid. The Commit Was Not.
An npm provenance attestation binds a tarball to a repository, a workflow and a commit. Reading the payload shows what it never claims.

npm audit signatures says this site’s dependency tree is in reasonable shape:
451 packages with verified registry signatures, 105 of them carrying verified
attestations. On July 14, 2026, five releases across four AsyncAPI packages were
published with attestations exactly as valid as those 105, and every one of them
shipped code no maintainer had approved.
The attestations were not forged, and nobody bypassed them. They were correct. That is not a contradiction, and seeing why means reading what an attestation actually contains — so this article reads one, field by field.
Provenance is on, and mostly unread
The check takes a few seconds. On this repository, with npm 11.19.0 on Node 24.20.0:
npm audit signaturesaudited 451 packages in 14s
451 packages have verified registry signatures
105 packages have verified attestationsTwo numbers, and the second one is the interesting one. A registry signature is npm attesting that it served these bytes. An attestation is a claim about where the bytes came from before npm ever saw them. The command reports how many packages carry one. It does not tell you what the claim says, and most people who run it have never looked.
The lockfile article left provenance at exactly this point, describing it as what closes the gap between a tarball and the repository it claims to come from. That is true, and it is worth being much more specific about which gap.
Five correct attestations on five malicious releases
Microsoft’s write-up of the AsyncAPI compromise states the outcome plainly:
All five malicious versions were published through npm trusted publishing using GitHub OIDC and carried valid provenance attestations. The attestations accurately identified the legitimate repositories, commits, and workflows that created the packages, even though the triggering commits were unauthorized.
The route in was a workflow misconfiguration — a pull_request_target job that
checked out an untrusted head commit while running in the base repository’s
security context, which exposed a bot token and allowed pushes to branches that
auto-publish. Three weeks later, on August 4, [email protected] went out at 09 UTC
as the first of eleven malicious releases across the cacheable family.
Snyk’s analysis
reaches the same boundary from the other side: “Provenance can faithfully attest
a build whose source or workflow context has already been compromised.”
These incidents are not identical and it is worth not merging them. Microsoft attributes the initial ChainDrop compromise to stolen maintainer credentials; the AsyncAPI entry point was repository push access obtained through the workflow. What they share is the step after: the project’s own release pipeline ran, honestly, over source it should never have been given.
What the payload for [email protected] says
The registry exposes attestations at a documented endpoint, and npm’s own documentation is careful about what they mean: establishing provenance “does not guarantee the package has no malicious code.” Each attestation is a DSSE envelope wrapping a base64 payload, so reading one takes a single decode:
curl -sS https://registry.npmjs.org/-/npm/v1/attestations/[email protected] \
| jq -r '.attestations[]
| select(.predicateType=="https://slsa.dev/provenance/v1")
| .bundle.dsseEnvelope.payload' \
| base64 -d | jq '.predicate.buildDefinition'{
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/heads/main",
"repository": "https://github.com/sigstore/sigstore-js",
"path": ".github/workflows/release.yml"
}
},
"internalParameters": {
"github": {
"event_name": "push",
"repository_id": "495574555",
"repository_owner_id": "71096353"
}
},
"resolvedDependencies": [
{
"uri": "git+https://github.com/sigstore/sigstore-js@refs/heads/main",
"digest": { "gitCommit": "7d2900eca1c22b3f87c13987c8d4b7c9a29b733a" }
}
]
}That is the whole build definition. Alongside it, runDetails names the builder
as https://github.com/actions/runner/github-hosted and records the workflow run
URL, and the statement’s subject carries the package name and a SHA-512 digest
of the tarball.
Every field commits to a build, none to a decision
Read the fields as claims and the boundary draws itself.
| Field | What it commits to |
|---|---|
subject.digest |
These exact tarball bytes |
externalParameters.workflow |
This repository, this workflow file, this ref |
internalParameters.github.event_name |
The kind of event that started the run |
resolvedDependencies[].digest.gitCommit |
The commit the build read |
runDetails.builder.id |
The build platform that executed it |
Every entry is a fact about machinery. repository says which repository, not
who may push to it. gitCommit identifies the commit, not its author and not
its reviewers. event_name says a push triggered the run, not that the push was
one anybody sanctioned. There is no field for a maintainer’s approval, because
the format does not model approval — it models a build.
That is a coherent design rather than an oversight. A build system can observe what it built from. It cannot observe whether a human meant it. The gap between those two is where all three of the 2026 compromises lived, and no amount of signing closes a gap the payload never spanned.
The registry returns two attestations, not one
Ask for a package’s attestations and two documents come back, which is easy to miss because the badge on npmjs.com is singular. The second is much smaller:
{
"name": "sigstore",
"version": "5.0.0",
"registry": "https://registry.npmjs.org"
}That is the publish attestation — the registry’s own record that this name and
version were published to it. The distinction is visible in how each one is
verified. Inspect the bundles and the provenance attestation carries a
certificate, a short-lived Sigstore certificate issued against the workflow’s
OIDC identity, while the publish attestation carries a publicKey, npm’s own.
Different signers, different claims: one says a build produced these bytes, the
other says the registry accepted them.
Neither signer inspected the code. Both are honest about that.
What provenance does close
It would be a bad reading of the incidents to conclude the attestation is theater. It rules out a specific and previously common failure: a tarball published from somewhere other than the declared build path. Before provenance, a package’s contents were whatever the publisher uploaded, and a maintainer laptop with a long-lived token was indistinguishable from CI. Now a consumer can require that a release came from a named workflow in a named repository, and detect a publish that did not.
It also makes a compromise legible after the fact. The AsyncAPI attestations named the commits, which is how the unauthorized pushes were identified at all. An attestation that points at a bad commit is more useful than no attestation pointing anywhere.
What it does not do is discharge the question of who can cause that workflow to run. Provenance moves trust from the publishing step to the repository’s access controls, and a policy that says “require provenance” without also saying anything about branch protection, workflow triggers or token scope has moved the problem rather than solved it.
Write the policy against the fields
If you are writing a rule for your organization, write it against what the
payload actually contains. Require an attestation, then check the repository and
workflow path are the ones you expect rather than merely present — a valid
attestation from an unexpected repository is a supply chain event on its own.
Record the gitCommit you installed, because during an incident that field
answers “what source is in my build” in seconds. And treat a dependency’s branch
protection and release trigger configuration as part of its risk profile, since
that is the layer the attestation delegates to.
Run npm audit signatures in CI. It is fast, and 105 of 451 is a number worth
watching move.
What I have not tested is the failure mode of verification itself: what a consumer sees when the transparency log is unreachable, or when a certificate has expired relative to the tarball’s publish time. Those paths matter for anything that gates a deploy on this check, and I do not yet know how they behave.
FAQ
Frequently asked
Does a provenance attestation mean a package is safe?
No. It means the tarball was produced by the workflow and commit named in the payload. npm states this directly: establishing provenance "does not guarantee the package has no malicious code." The attestation is evidence about a build, and a build can faithfully compile source that should never have been merged.
What is the difference between npm audit and npm audit signatures?
They answer different questions. npm audit compares your installed versions against a database of known vulnerabilities. npm audit signatures checks registry signatures and provenance attestations — who published these bytes, and from where. A package can pass one and fail the other, and neither one reads the code.


