Skip to content

Switch npm platform claims from token verification to signed-claim model (matching PyPI) #107

@bordumb

Description

@bordumb

Problem

npm platform claims currently use token verification via registry.npmjs.org/-/whoami. This is vulnerable to the same attack vector as the LiteLLM supply-chain compromise (March 24, 2026): an attacker who steals an npm token from CI can verify as the maintainer and claim their namespace on auths.

The LiteLLM attacker stole PyPI credentials from a compromised Trivy GitHub Action in CI/CD. If the same pattern targets npm tokens (which are routinely stored as CI secrets), the current npm claim flow would let the attacker claim namespaces under the real maintainer's identity.

Current npm flow (vulnerable)

  1. User runs auths id claim npm
  2. CLI prompts for npm access token
  3. CLI calls HttpNpmAuthProvider::verify_token()GET registry.npmjs.org/-/whoami with Bearer auth
  4. Server receives the token, independently verifies via npm whoami, stores the claim
  5. Attack: stolen npm token → attacker verifies as maintainer → claims namespace

PyPI flow (secure, already implemented)

  1. User runs auths id claim pypi
  2. CLI prompts for PyPI username (no token)
  3. CLI creates a signed platform claim with the device signing key (in platform keychain, not CI)
  4. Server verifies the Ed25519 signature only — trusts the self-reported username
  5. At namespace claim time, the PyPI verifier checks the public pypi.org/pypi/{package}/json API for maintainer status
  6. Attack blocked: stolen PyPI token → attacker can't produce a valid signed claim (needs the device key from macOS Keychain)

Proposed change

Switch npm to the same model as PyPI:

CLI (crates/auths-cli/src/commands/id/claim.rs)

  • Remove token prompt from ClaimPlatform::Npm handler
  • Prompt for npm username instead
  • Remove HttpNpmAuthProvider usage

SDK (crates/auths-sdk/src/workflows/platform.rs)

  • Remove npm_token parameter from claim_npm_identity()
  • Change proof URL format from npm-token:{token}:{claim} to npm-claim:{claim}

Infra (crates/auths-infra-http/src/npm_auth.rs)

  • Delete HttpNpmAuthProvider (dead code after this change)
  • Remove from lib.rs exports

Server (auths-cloud/crates/auths-registry-server/src/services/proof_verification.rs)

  • Replace verify_npm_token_proof() with verify_npm_claim_proof() (signature-only, no whoami call)
  • Update proof dispatch in routes/identity.rs to match npm-claim: prefix

Server (auths-cloud/crates/auths-registry-server/src/routes/identity.rs)

  • Update proof URL prefix check from npm-token: to npm-claim:
  • Update proof_type DB value from "npm-token-proof" to "npm-claim-proof"

Security model after change

Layer What it checks Can stolen token bypass?
Platform claim Ed25519 signature from device key No — key is in platform keychain
Namespace claim Public npm registry maintainers API No — can't fake the public API

The npm namespace verifier (npm_verifier.rs) already checks registry.npmjs.org/{package} for maintainers — no changes needed there.

Why this is more secure

The LiteLLM attack chain was: compromised CI tool → stolen PyPI token → malicious publish. The same chain with npm tokens is: compromised CI tool → stolen npm token → auths id claim npm with stolen token → claim namespace.

With the signed-claim model, the attacker needs the device signing key (not stored in CI, lives in macOS Keychain / Linux Secret Service) to produce a valid platform claim. Stealing an npm token from CI gives them nothing.

Files to modify

File Change
crates/auths-cli/src/commands/id/claim.rs Prompt for username, remove token prompt
crates/auths-sdk/src/workflows/platform.rs Remove npm_token param, change proof format
crates/auths-infra-http/src/npm_auth.rs Delete file
crates/auths-infra-http/src/lib.rs Remove HttpNpmAuthProvider export
auths-cloud/.../services/proof_verification.rs Replace verify_npm_token_proof with signature-only verifier
auths-cloud/.../routes/identity.rs Update prefix check and proof_type

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions