Manav.id
Standards ยท 12 min read

Sigstore signs the build. Who signed the decision to publish?

The software supply chain has excellent machine provenance and almost no human provenance. Sigstore and SLSA prove which pipeline built an artifact from which commit, which is real and valuable. Neither answers the question a self replicating worm exploits: did a person decide to release this version?

The email arrives on a Tuesday and it is not exciting, which is the point. It says your npm two factor enrollment needs to be renewed, and it comes from a domain that is one character away from the real one, and the login page it takes you to is a perfect copy because it is a perfect copy: a reverse proxy sitting in front of the genuine site, relaying every byte in both directions.

You type your password. You approve the two factor prompt. The real site authenticates you, correctly, because you really did authenticate. The proxy in the middle keeps the session token that comes back.

You maintain four packages. Nothing famous. A logging helper, a small date utility, a wrapper around an API you needed once, and a colour formatting library you wrote in 2019 that somehow ended up in the dependency tree of half the ecosystem. Roughly nine million weekly downloads between them, which is a number you stopped thinking about years ago.

Within the hour, malicious versions of all four are published. You did not publish them. Nobody published them, in the sense of a person deciding. A script held a valid token, and the registry does what it does when a valid token arrives: it accepts the upload and starts serving it.

Then it gets worse, in a way that supply chain attacks had not previously managed. The published code runs on install, harvests the developer credentials, cloud keys, and continuous integration secrets of everyone who installs it, and uses those stolen credentials to publish again. From other people's accounts. To other people's packages. Nobody has to phish anybody a second time. The thing propagates on its own.

Short answer: Package registries treat a valid token as authorization to publish, so a stolen credential publishes exactly like a maintainer. Sigstore and provenance attestations prove which pipeline built an artifact from which commit, which is genuinely valuable and answers a different question. Nothing in the chain requires that a human decided to release this version. A per release signature from the maintainer's enrolled device would break a self replicating worm at its first hop.

What actually happened with the Shai-Hulud worm?

In September 2025 the npm ecosystem met the first widely documented self replicating supply chain worm. Security teams at Kaspersky, Elastic, Sygnia, Checkmarx and ReversingLabs published analyses through late 2025 and into 2026, and while the details differ at the edges, the shape they describe is consistent.

Hop one: a credential, not a vulnerability

The entry point was a compromised maintainer credential. Not a flaw in npm. Not a bug in a package. A person's account, obtained the way accounts are usually obtained now, which is by relaying a real login through a proxy and keeping the session that comes out the far end.

This matters for the whole argument, so it is worth sitting with. There was no software vulnerability to patch at hop one. The registry behaved exactly as designed. The token was real, the account was real, the upload was well formed. Every technical control in the path returned true.

Hop two: the payload that recruits

What made this different from every previous npm incident was what the malicious versions did once installed. Rather than simply stealing from the end user, the payload went looking for the things that let it publish: npm tokens, GitHub credentials, cloud provider keys, secrets sitting in continuous integration environments. Analyses describe it using workflow automation, including GitHub Actions, to spread into repositories that consumed an infected package.

Collect enough publishing credentials and you no longer need a phishing campaign. Each compromised developer machine or pipeline becomes a launch point for the next round of publishes, from accounts that have never been phished, to packages the original attacker never targeted.

That is the difference between an attack and a worm, and it is why the counts climbed the way they did. Published analyses describe hundreds of affected packages across the initial waves, and later variants in 2026, reported by Elastic Security Labs among others, reached co owned packages whose combined download volume was described as exceeding a billion installs per month.

The context it landed in

It did not arrive into a calm period. On 8 September 2025, a separate compromise hit eighteen widely used packages including debug and chalk, with a payload aimed at cryptocurrency activity in the browser. Two utility packages that most developers could not name the maintainer of, sitting underneath an enormous fraction of the JavaScript ecosystem.

Set alongside the earlier canonical incidents, a pattern emerges. In 2018 the event-stream package was compromised after a maintainer handed it to a volunteer. In 2021 ua-parser-js was hijacked through a maintainer account. In 2024 the xz backdoor was inserted by a contributor who spent years earning commit rights. Different techniques, one constant: the attacker acquires the right to publish, and then publishes.

Why did none of the existing controls stop it?

This is where the story becomes uncomfortable, because the ecosystem is not unprotected. It has spent years building genuinely good machinery. Let us take each piece seriously and ask exactly what it proves.

Registry two factor authentication

Two factor authentication protects a login event. Adversary in the middle phishing kits do not attack the second factor, they let you complete it and steal the session that results. The industry has watched this at scale: the disruption of the Tycoon 2FA phishing service announced in March 2026 by Microsoft, Europol and partners involved a platform reported to have affected more than ninety six thousand victims across hundreds of seized domains. We wrote about the mechanism in detail in your MFA worked perfectly, the attacker was already inside the session.

So two factor authentication answers "did someone complete a login challenge for this account". It does not answer "is the entity now holding this session the human who completed it".

Trusted publishing from continuous integration

Trusted publishing is a real improvement. Instead of a long lived token sitting in a repository secret waiting to be stolen, the continuous integration job proves its identity to the registry using short lived OpenID Connect credentials, and the registry issues publish rights scoped to that job.

It removes an entire class of static credential theft, and it should be adopted broadly. It also relocates the question rather than answering it. Trusted publishing proves that a particular workflow, in a particular repository, published this version. A worm that has compromised the repository or the workflow is a particular workflow in a particular repository. It publishes with impeccable credentials.

Provenance attestations

Sigstore, SLSA and npm provenance are the strongest work in this area and they deserve credit rather than a strawman. Sigstore solved a problem that had blocked artifact signing for a decade: key management. Nobody wanted to hold long lived signing keys, so almost nobody signed anything. Sigstore replaced held keys with ephemeral certificates bound to an identity from an OpenID Connect provider, and recorded the whole thing in a public transparency log so that a signature cannot be issued in secret.

What you get from that is a verifiable statement of the form: this artifact, with this digest, was built by this workflow, from this repository, at this commit. That is a genuine and substantial claim. It kills a large family of attacks where somebody hands you a binary and asserts it came from a repository you trust.

Now read the claim again and notice which word is missing. Every noun in it is a machine. A workflow. A repository. A commit. A digest. The claim is complete and accurate and it never mentions a person.

A worm that controls the repository produces provenance that verifies. Not forged provenance, real provenance, honestly describing a build that genuinely happened in the pipeline the attacker now controls. Provenance is not being defeated here. It is answering the question it was designed to answer, correctly, and that question is not the one under attack.

Software composition analysis

Scanners find known bad packages after somebody has identified them as bad. The window between publication and detection is when the installs happen, and for a fast moving worm that window is where the entire event takes place.

ControlWhat it provesWhat it cannot prove
Registry passwordSomeone knew a secretWho they were
Two factor authenticationSomeone completed a login challengeThat the session holder is still that person
Trusted publishing (OIDC)This workflow, in this repository, published itThat a human wanted this version released
Sigstore and npm provenanceThis artifact was built by this workflow from this commitThat a human decided to publish the result
Transparency logThe signature was issued publicly and cannot be hiddenThat the signing was intended
Composition analysisThis version matches a known bad signatureAnything at all before somebody notices
Human signed releaseAn enrolled maintainer approved this exact version and digestThat the code is good, or that the maintainer read it

The factory with immaculate records and no one in charge

Imagine a factory with the best logistics operation you have ever seen. Every item that leaves has a tamper evident label recording which production line built it, from which batch of raw material, at what time, under which quality process. The records are public, append only, and cryptographically verifiable. You can take any item off any shelf anywhere in the world and trace it back to the exact machine that made it.

Now ask the factory a simple question: who decided to ship this?

And the answer is that nobody knows, because that was never recorded, because the shipping decision was made by whoever was holding the door badge at the time, and the badge does not have a face on it.

That is the software supply chain in 2026. We have superb provenance for the manufacturing and no provenance at all for the decision. And decisions are what attackers actually want. They do not want to build your package. They want your package released with their code inside it, and every existing control is happy to help them do it as long as they are holding a badge.

This is the gap we call Session-Inherited Authorization, and the release pipeline is where it costs the most, because a single unauthorized decision propagates to every machine that runs an install.

What would a human signed publish actually look like?

The release of a new version becomes an action that requires a signature from the maintainer's enrolled device, over the content of that specific release. Not a login. Not a session. A signature over the thing being published.

The payload is small and boring, which is what you want in something that has to be identical on both sides of a verification:

{
  "action":         "package.publish",
  "registry":       "registry.npmjs.org",
  "package":        "colour-format",
  "version":        "4.2.1",
  "tarball_digest": "sha512-9f2c4a...e18b",
  "maintainer":     "npm:aparna",
  "issued_at":      "2026-09-05T09:14:07Z",
  "nonce":          "7d41e0c2ab93"
}

Those bytes are canonicalised, hashed, and the hash becomes the challenge for a signature on a device the pipeline does not control. The maintainer sees the package name, the version, and the digest rendered on their own phone, confirms that yes, they meant to cut 4.2.1 right now, and approves. The resulting receipt travels with the package.

Verification is the part that has to be dull:

digest = sha512(tarball_bytes)
hash   = sha256(canonical_json(payload))

assert payload.tarball_digest == digest        # this exact artifact
assert receipt.challenge == hash               # signed THIS release
assert verify_ed25519(receipt.sig,
                      receipt.challenge,
                      published_key(maintainer))
assert receipt.version == manifest.version     # no substitution

accept(package)

Two properties of that verification matter. First, it composes with provenance rather than replacing it: you check the Sigstore attestation to learn which pipeline built the artifact, and you check the release receipt to learn which human released it. Two different claims, both useful, neither redundant.

Second, the check needs no network call. The receipt verifies against a published key, so a consumer, an internal scanner, or an auditor two years from now can establish that a human approved version 4.2.1 without asking the registry, without asking us, and without either of us still being around. That is the same property we describe for records generally in what is an attestation chain.

Now replay the worm against it. The stolen session still holds a valid token. The pipeline still builds. The provenance still verifies. And the publish is refused, because there is no receipt, because producing one requires a device in a maintainer's pocket that the worm has never touched. The worm cannot complete hop one, so there is no hop two, so there is no self replication.

Nobody is going to sign four hundred releases a week

Good. They should not.

This is the objection that kills naive versions of this proposal, and it deserves a real answer rather than an appeal to discipline. Modern release practice is automated on purpose. Dependency bumps cut patch releases. Merge queues trigger builds. Some healthy projects publish several times a day, and asking a volunteer maintainer to approve each one on their phone is how you get the control disabled in a fortnight, or worse, delegated to a bot, which returns you precisely to where you started.

The answer is to gate by blast radius rather than by event. The question is not "was this a release" but "how many machines does this reach if it is wrong".

Release classExampleSensible policy
Very high reachPackage with millions of weekly downloads, or thousands of dependentsHuman signature on every release, no exceptions
High reach, routine changePopular package, automated patch or dependency bumpPre signed delegation with narrow scope: patch versions only, expiring, revocable. Anything else escalates
Major or breaking changeAny package, major version or new maintainerHuman signature, always
Low reachInternal or low dependent packageProvenance alone is proportionate
First release from a new device or accountAnyHuman signature with liveness, because this is the takeover signal

The delegation in row two is not a loophole, it is the same primitive with an explicit scope. A maintainer signs an object that says: this pipeline may publish patch versions of this package for the next thirty days, and nothing else. It carries fields for the permitted actions, the constraints, a validity window, a limit on further delegation, and an identifier that revokes it. That is a statement a consumer can verify and a maintainer can withdraw, which is categorically different from a token that simply exists until someone notices it should not.

An organisation that wants this today does not have to wait for a public registry. The place to start is your own private registry, where you set policy, and the same idea appears at the commit level in my agent shipped this. The signed code lab demonstrates the flow, and the integration docs cover the signing and verification calls.

What this does not fix

Several things, and pretending otherwise would be its own kind of supply chain problem.

What to do this week

  1. Count the packages your organisation installs that have exactly one maintainer with publish rights. That number is your real exposure, and almost nobody has it written down.
  2. Turn on trusted publishing wherever you publish. It is a genuine improvement and it costs an afternoon.
  3. Verify provenance in continuous integration rather than merely producing it. A large share of published attestations are never checked by anyone, which makes them decoration.
  4. Write your policy for what happens when a dependency publishes without provenance. Deciding during an incident is how organisations end up shipping the malicious version because the pipeline was red and the release was late.
  5. For internal packages in your private registry, require a human signature on any release of anything more than twenty other teams consume. Start with one package and measure the friction honestly.
  6. Rehearse credential rotation across npm, GitHub, and your cloud provider, timed. The Shai-Hulud payload targeted exactly these, and the remediation cost is dominated by how long rotation takes.
  7. If you maintain a widely used package, enroll a second device for signing now, so that a lost phone never becomes an argument for turning the control off.

The one sentence version

The supply chain security industry built excellent provenance for machines and quietly left the human out of the record, and a worm is simply the discovery that when nobody has to decide, nobody has to be compromised twice.

Frequently asked questions

How could the Shai-Hulud npm worm have been stopped? By requiring each release to be signed from the maintainer's own enrolled device, over the package name, version, and tarball digest. The worm held stolen tokens and stolen sessions, which is enough to satisfy a registry but not enough to produce a device bound signature. Without a valid release receipt at the first hop, there is no propagation.

Does npm provenance prevent supply chain attacks? It prevents an important class of them. Provenance proves an artifact was built by a specific workflow from a specific commit, which defeats attacks that substitute a binary for one you trust. It does not prove a human decided to publish, so an attacker who controls the repository or the pipeline produces provenance that verifies correctly.

What is trusted publishing? Trusted publishing lets a continuous integration job authenticate to a registry with short lived OpenID Connect credentials instead of a long lived token stored in repository secrets. It removes a large class of static credential theft and is worth adopting. It proves which workflow published, which is a different claim from which human released.

Is this a replacement for Sigstore? No, it composes with it. Sigstore answers which pipeline built this artifact from which commit. A release receipt answers which enrolled human approved publishing it. A consumer can check both offline, and the ideal outcome is a human signature extension that rides alongside a Sigstore attestation rather than a competing format.

Would this force maintainers to sign every release? No, and any proposal that does will be rejected, correctly. The policy should follow blast radius. Very high reach packages and all major or breaking releases get a signature. Routine automated patch releases run under a narrow, expiring, revocable delegation. Low reach packages are fine with provenance alone.

Does a human signed release mean the code is safe? It does not. A signature proves a specific human approved releasing a specific artifact. It says nothing about whether the code was reviewed or is correct. What it provides is accountability and, more importantly here, a break in worm propagation, because stolen credentials alone can no longer publish.

Can I use this today on public npm? Not on public registries. There is no npm plugin for this, and public registry adoption is a proposal rather than a shipped integration. The practical starting point is a private registry your organisation already controls, where you can set the policy for internal packages and measure the friction before advocating upstream.

Sources

  1. Securelist (Kaspersky), analysis of the Shai-Hulud worm infecting npm packages. securelist.com
  2. Elastic Security Labs, reporting on the later Shai-Hulud wave through a compromised maintainer and affected package download volumes. elastic.co/security-labs
  3. Checkmarx research describing Shai-Hulud as a self replicating supply chain attack using GitHub Actions. checkmarx.com/blog
  4. ReversingLabs and Sygnia analyses of the npm worm and maintainer credential compromise. reversinglabs.com/blog
  5. Sigstore project documentation on keyless signing and transparency logs. sigstore.dev
  6. SLSA supply chain integrity framework. slsa.dev
  7. npm documentation on provenance and trusted publishing. docs.npmjs.com
  8. Microsoft Security Blog on the disruption of the Tycoon 2FA phishing service, March 2026. microsoft.com/en-us/security/blog
  9. NIST Secure Software Development Framework, SP 800-218. csrc.nist.gov
Provenance proves the pipeline. The worm has a pipeline. The missing signature is the maintainer's, on this version, from a device the worm never touched.