Skip to content

feat: trust signing keys published by a custom npm registry - #885

Open
opswithranjan wants to merge 1 commit into
nodejs:mainfrom
opswithranjan:feat/trust-custom-registry-signing-keys
Open

feat: trust signing keys published by a custom npm registry#885
opswithranjan wants to merge 1 commit into
nodejs:mainfrom
opswithranjan:feat/trust-custom-registry-signing-keys

Conversation

@opswithranjan

@opswithranjanopswithranjan commented Aug 5, 2026

Copy link
Copy Markdown

Refs #884

Problem

When COREPACK_NPM_REGISTRY points at a registry that re-signs the packages it serves, Corepack rejects the package outright:

Usage Error: The package was not signed by any trusted keys: {
"signatures": [ { "keyid": "SHA256:5mJAtUoc…" } ],
"trustedKeys": [ "SHA256:jl3bwswu…", "SHA256:DhQ8wR5…" ]
}

Corepack fetches metadata and tarballs from the configured registry, but verifySignature only ever compares against the npm keys bundled in config.json (or a manually supplied COREPACK_INTEGRITY_KEYS). The registry's own signing keys are never consulted, so a package it signed can never verify.

This isn't practically workaroundable for affected users: many consumers (CI images, Dependabot jobs) can't inject COREPACK_INTEGRITY_KEYS into the environment, and .corepack.env isn't loaded by the prepare / install subcommands (#741).

What this changes

When the bundled keys don't already cover the signature and a non-default COREPACK_NPM_REGISTRY is configured andCOREPACK_INTEGRITY_KEYS isn't set, Corepack now fetches that registry's keys from <registry>/-/npm/v1/keys (the endpoint defined by npm's registry-signature spec) and trusts them in addition to the bundled ones.

ConfigurationTrusted keysExtra request
COREPACK_INTEGRITY_KEYS setthat value (override still wins)no
default npm registrybundled npm keysno
custom registry, signature matches bundled keysbundled npm keysno
custom registry, signature doesn't matchbundled + registry-published keysyes, once per registry

Added to, not replaced. A proxying registry serves a mix: versions it has stored are signed with its own key, while versions it hasn't pulled yet are passed through from upstream with npm's original signature intact. Corepack can be asked for either, so both key sets have to be trusted. Merging also means the change can only ever add trust, where replacing the bundled keys could stop legitimately npm-signed packages from verifying.

Lazy on purpose. The request only happens on the path that currently hard-fails, so every configuration that works today issues exactly the same requests as before.

Fails safe, never open. If the endpoint is unreachable, returns no keys array, returns an empty one, or every entry is malformed, Corepack keeps verifying against the bundled keys and reports the usual error. It never falls back to skipping verification.

Trust model

The keys are fetched from the same registry, over the same channel and with the same credentials, that serves the tarball Corepack is about to unpack and execute. Consulting its published keys is strictly less privilege than the code execution Corepack already grants it, so this doesn't widen the trust boundary. It also matches what the npm CLI does: it verifies against the configured registry's keys, not a hardcoded set.

Testing

tests/npmRegistryUtils.test.ts grows from 7 to 19 cases, covering: verifying against a registry-published key; sending registry credentials on the keys request; trailing-slash normalisation; ignoring malformed entries; fetching at most once per registry; not fetching when no custom registry is set / COREPACK_INTEGRITY_KEYS is set / the bundled keys already match; and falling back rather than skipping verification when the endpoint fails or publishes nothing usable.

Verified end to end against Cloudsmith that re-signs what it serves. The registry below is public, so this reproduces with no credentials:

export COREPACK_NPM_REGISTRY="https://npm.cloudsmith.io/opswithranjan/npm"unset COREPACK_INTEGRITY_KEYS
COREPACK_HOME=$(mktemp -d) corepack prepare npm@11.16.0 --activate
Command (identical env, only the branch differs)mainthis branch
corepack prepare npm@11.16.0 --activateexit 1, not signed by any trusted keysexit 0
corepack prepare pnpm@10.34.5 --activateexit 1, same errorexit 0

Both packages in that registry are signed with its own key (SHA256:5mJAtUoc…), which is exactly what <registry>/-/npm/v1/keys publishes and what Corepack currently has no way to trust:

curl -s https://npm.cloudsmith.io/opswithranjan/npm/-/npm/v1/keys
curl -s https://npm.cloudsmith.io/opswithranjan/npm/npm/11.16.0 | jq '.dist.signatures'

Notes

  • A trailing slash on COREPACK_NPM_REGISTRY would have produced //-/npm/v1/keys; npmjs tolerates that path but not every registry does, so it's trimmed before the keys path is appended.
  • Key expiry is still not enforced, matching existing behaviour (the bundled npm key with expires: 2025-01-29 is already past that date and remains trusted). Out of scope here.

When `COREPACK_NPM_REGISTRY` points at a registry that re-signs the
packages it serves, the signature is made with that registry's own key,
which is not part of the npm keys Corepack bundles. Verification then
fails with "The package was not signed by any trusted keys", and there is
no way to work around it in environments where `COREPACK_INTEGRITY_KEYS`
cannot be set.
Fall back to the keys the configured registry publishes at
`/-/npm/v1/keys` when the bundled keys don't already cover the signature,
and trust them in addition to the bundled ones so that packages which
keep their original npm signature continue to verify. An explicit
`COREPACK_INTEGRITY_KEYS` still takes precedence, and the extra request
only happens when verification would otherwise fail, so setups that work
today are unaffected. When the keys cannot be fetched or none of them are
usable, Corepack keeps verifying against the bundled keys rather than
skipping the check.
Refs: nodejs#884
@opswithranjan

opswithranjan commented Aug 5, 2026

Copy link
Copy Markdown
Author

For context on where this comes from: I work at Cloudsmith, and this reached us through several customers whose builds break against Corepack. It isn't specific to us though. Any registry that re-signs the packages it serves hits the same wall, and the fix is built on the standard /-/npm/v1/keys endpoint rather than anything Cloudsmith-specific.

To make this verifiable rather than just described, here is a public registry that reproduces it. No credentials needed.

The setup: The registry publishes one signing key, and the packages it serves are signed with that key rather than npm's:

R=https://npm.cloudsmith.io/opswithranjan/npm
# key the registry publishes
curl -s "$R/-/npm/v1/keys"| jq -r '.keys[].keyid'# -> SHA256:5mJAtUoc0TI7kFjJ8NoV+LnJqHmAsJSv+r7wbMoQrBs# key that actually signed the package
curl -s "$R/npm/11.16.0"| jq -r '.dist.signatures[].keyid'# -> SHA256:5mJAtUoc0TI7kFjJ8NoV+LnJqHmAsJSv+r7wbMoQrBs# npm's keys, for contrast: neither of these is the above
curl -s https://registry.npmjs.org/-/npm/v1/keys | jq -r '.keys[].keyid'

Reproduce the failure on main:

yarn build
unset COREPACK_INTEGRITY_KEYS
export COREPACK_NPM_REGISTRY=https://npm.cloudsmith.io/opswithranjan/npm
COREPACK_HOME=$(mktemp -d) node dist/corepack.js prepare npm@11.16.0 --activate
Usage Error: The package was not signed by any trusted keys: {
"signatures": [ { "keyid": "SHA256:5mJAtUoc…" } ],
"trustedKeys": [ "SHA256:jl3bwswu…", "SHA256:DhQ8wR5…" ]
}

Same command on this branch:

yarn build
COREPACK_HOME=$(mktemp -d) node dist/corepack.js prepare npm@11.16.0 --activate

Exits 0 and activates. Corepack prints nothing beyond the Preparing ... line on success, so a quiet run is the pass. pnpm@10.34.5 in the same registry behaves the same way (fails on main, passes here).

A fresh COREPACK_HOME matters on each run, otherwise the cached package manager means nothing is downloaded or verified. COREPACK_INTEGRITY_KEYS must be unset, since setting it takes precedence by design and bypasses the code path in question.

Happy to adjust the approach if you'd rather this were shaped differently.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@opswithranjan