Manav.id
Developer · 4 min read

Package publishing authenticates the session, not the release

Package publishing authenticates the session, not the release

A maintainer's session token is lifted from their machine. Hours later a new version of a widely used package appears, published by their account, with a modified build script. No authentication failure occurred anywhere.

Why does two-factor authentication not protect a package release?

Because it protects the login, and publishing happens later through a token the session already holds. A stolen session or a leaked automation token reaches the publish endpoint without touching the second factor again, and the registry sees a perfectly ordinary release.

Key takeaways
  • Registry two-factor authentication protects login. Publish tokens and live sessions bypass it entirely.
  • The published tarball can differ from the repository tag, and most consumers never compare them.
  • Binding a signature to the artefact digest at publish time makes a stolen session insufficient to release.

The three publish paths

Login with second factorstrongSession or token issuedlong-livedToken stolen or leakedphishing, CI logs, laptopPublish endpoint calledno second factor hereRelease propagatesto every consumer
The gap between the two is where every registry compromise lives.
PathWhat authorises itReaches 2FA?
Interactive publish from a logged-in sessionSession tokenSometimes, if the registry prompts
Automation token in CIA long-lived tokenNo
Trusted publishing via CI identityAn OIDC identity from the build systemNo — by design

Trusted publishing is a genuine improvement over long-lived tokens: there is no secret to steal. It binds publication to a workflow, which means it answers provenance and leaves authority to whoever can trigger that workflow.

The tarball-versus-tag gap

Most ecosystems publish a built artefact, not the repository contents. The build happens on the publisher's machine or runner, and what lands in the registry is whatever that process produced.

A consumer installing the package receives the tarball. The repository tag they might inspect is a different object. Reproducible builds and provenance attestation address this where they are deployed, and coverage remains partial.

The practical consequence: an attacker with publish capability does not need to touch the repository at all. The malicious code can exist only in the published artefact.

What binding at publish looks like

# The registry requires a signature over what is being published

POST /packages/example-lib/versions
{
  "version": "4.2.1",
  "tarball_sha256": "9c1f8a3e...d7b2",
  "receipt": {
     "statement": {
       "action":  "package.publish",
       "package": "example-lib",
       "version": "4.2.1",
       "digest":  "9c1f8a3e...d7b2",
       "repository": "github.com/org/example-lib",
       "commit": "7f3a91c8e2b7d4056891fa2c73e1b9d40"
     },
     "signature": "...",          # maintainer's authenticator
     "user_verified": true
  }
}

# Registry verifies: signature valid, credential enrolled to a
# maintainer of this package, digest matches the uploaded tarball.

A stolen session can reach the endpoint and cannot produce the signature. The maintainer's authenticator is required, with a user-verification gesture, at publish time.

The automation objection

Real, and it is the reason this has not happened already. Most packages publish from CI, and a human signature per release means someone must be present at every release.

Three positions, and they are not mutually exclusive.

  1. Tier by download volume. A package with millions of weekly installs is infrastructure. A human signature per release is proportionate there and absurd for a package with forty users.
  2. Sign the release, not the build. The maintainer signs a statement authorising a specific version from a specific commit, ahead of the build. CI produces the artefact and the registry verifies the digest matches what was authorised.
  3. Delegate with bounds. A maintainer signs a scoped delegation — this workflow may publish patch versions of this package for ninety days — and the chain verifies to them.

The third preserves automation and is the realistic answer for most maintainers. It also creates something that currently does not exist: a record of which human stands behind an automated release.

What a consumer could then check

Today an installation checks an integrity hash against a lockfile, which confirms you got the same bytes as last time and says nothing about who published them.

The third question is the one that would catch the most incidents. A maintainer account publishing under a credential it has never used before is a detectable anomaly that nobody can currently detect.

The maintainer burden problem

Any proposal that increases the cost of maintaining open source deserves scepticism. Maintainers are volunteers, and security requirements imposed on them have a history of driving people away.

This is why tiering matters and why delegation matters. The design that works is one where a maintainer signs once, infrequently, for a bounded window, and their existing automation continues to function. A design that demands a hardware key touch at every release will simply not be adopted.

A worked example: the tarball-versus-tag gap

What a consumer can and cannot check
ClaimCheckable today?
This version exists in the registryYes
A git tag with this name existsYes
The tarball matches that tagRarely — build steps intervene
A named maintainer intended this releaseNo
A maintainer signed this artefact hashOnly if they were asked to

The fourth row is the one that matters and the one nothing currently produces. Reproducible builds help with the third; they do not touch the fourth, because intent is not a build property.

Objections and honest limits

“This breaks automated releases.” It changes them. A maintainer signs a release intent — version, artefact hash, date window — and automation publishes within it. The human signs once per release, not once per commit.

“Maintainer burden is already the problem.” It is, and this is the strongest objection. The answer is scope: sign releases, not merges, and make the signing step take seconds on a device the maintainer already carries.

Binding publish to a human

  1. Require a signature at the publish endpoint. Not at login.
  2. Sign the artefact hash, not the version string. The tarball is what consumers get.
  3. Allow a release-intent grant for automation. Bounded by version and time window.
  4. Publish maintainer keys where consumers can fetch them. Otherwise nobody can check.
  5. Keep the signature retrievable with the release. Years later, by anyone.

Terms used here

Session stealing
Theft of an authenticated session or token, which bypasses the login-time second factor entirely.
Release intent
A signed statement that a named maintainer meant to publish a specific artefact.
Tarball-versus-tag gap
The gap between what is in source control and what is actually distributed.

Frequently asked questions

Doesn't registry 2FA already cover this? It covers login. Publish tokens and live sessions reach the publish endpoint without a second factor, which is how most of these incidents occur.

Isn't trusted publishing the answer? It removes the stealable long-lived token, which is a real improvement. It binds publication to a workflow, so authority still rests with whoever can trigger that workflow.

Won't this burden maintainers? It would, if applied uniformly. Tier by download volume and allow bounded delegation so existing automation keeps working with one infrequent signature behind it.

What could consumers verify that they cannot today? Whether a version was signed by a credential enrolled to a maintainer, and whether it is the same credential as previous versions. A sudden change is a detectable signal.

Does two-factor authentication help at all? It protects the login. Publishing happens later through a token the session already holds, which is where compromises occur.

Does this break CI-driven releases? No. The maintainer signs a bounded release intent and automation publishes within it.

What could a consumer then check? That a named maintainer signed this artefact hash — the claim nothing currently supports.

Where this fits in Manav

Manav binds the publish endpoint to a human signature over the artefact hash, with bounded grants so automation still works.

See publish gating →

Sources and further reading