Uh oh!
There was an error while loading. Please reload this page.
feat: trust signing keys published by a custom npm registry - #885
feat: trust signing keys published by a custom npm registry#885opswithranjan wants to merge 1 commit into
Conversation
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
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 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 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 --activateSame command on this branch: yarn build
COREPACK_HOME=$(mktemp -d) node dist/corepack.js prepare npm@11.16.0 --activateExits 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 Happy to adjust the approach if you'd rather this were shaped differently. |
Refs #884
Problem
When
COREPACK_NPM_REGISTRYpoints at a registry that re-signs the packages it serves, Corepack rejects the package outright:Corepack fetches metadata and tarballs from the configured registry, but
verifySignatureonly ever compares against the npm keys bundled inconfig.json(or a manually suppliedCOREPACK_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_KEYSinto the environment, and.corepack.envisn't loaded by theprepare/installsubcommands (#741).What this changes
When the bundled keys don't already cover the signature and a non-default
COREPACK_NPM_REGISTRYis configured andCOREPACK_INTEGRITY_KEYSisn'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.COREPACK_INTEGRITY_KEYSsetAdded 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
keysarray, 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.tsgrows 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_KEYSis 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:
maincorepack prepare npm@11.16.0 --activatenot signed by any trusted keyscorepack prepare pnpm@10.34.5 --activateBoth packages in that registry are signed with its own key (
SHA256:5mJAtUoc…), which is exactly what<registry>/-/npm/v1/keyspublishes and what Corepack currently has no way to trust:Notes
COREPACK_NPM_REGISTRYwould have produced//-/npm/v1/keys; npmjs tolerates that path but not every registry does, so it's trimmed before the keys path is appended.expires: 2025-01-29is already past that date and remains trusted). Out of scope here.