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.

A dial indicator on a magnetic base, its stylus resting on a machined steel block clamped in a bench fixture, with one blank manila tag lying on the table beside it.

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:

bash
npm audit signatures
audited 451 packages in 14s

451 packages have verified registry signatures

105 packages have verified attestations

Two 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:

bash
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'
json
{
	"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:

json
{
	"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.

Arrow keys to move, Enter to open.