Repository navigation
pnpm audit signatures verifies an off-origin redirect of the signing-keys request with the registry's scoped cafile #16541
Description
Verify latest release
- I verified that the issue exists in the latest pnpm release
pnpm version
No response
Which area(s) of pnpm are affected? (leave empty if unsure)
CLI
Link to the code that reproduces this issue or a replay of the bug
No response
Reproduction steps
-
Create a private CA and a certificate for localhost:
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem -days 30 -subj "/CN=Repro Private CA" openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=localhost" printf "subjectAltName=DNS:localhost,IP:127.0.0.1\n" > san.ext openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 30 -extfile san.ext
-
Save this as
server.mjsand runnode server.mjs. It is a registry behind the private CA that proxies registry.npmjs.org, except that the signing-keys endpoint redirects to npmjs, as GitLab's npm package registry does:import { readFileSync } from 'node:fs'; import { createServer } from 'node:https'; const upstream = 'https://registry.npmjs.org'; createServer( { key: readFileSync('server.key'), cert: readFileSync('server.pem') }, async (req, res) => { if (req.url.endsWith('/-/npm/v1/keys')) { res.writeHead(302, { location: `${upstream}/-/npm/v1/keys` }); res.end(); return; } const response = await fetch(upstream + req.url, { headers: { accept: req.headers.accept ?? '*/*' }, }); res.writeHead(response.status, { 'content-type': response.headers.get('content-type') ?? 'application/json', }); res.end(Buffer.from(await response.arrayBuffer())); }, ).listen(8443, '127.0.0.1');
-
In another directory, create a project whose
cafileis scoped to that registry only, then install and audit:echo '{ "name": "repro", "private": true, "dependencies": { "is-number": "7.0.0" } }' > package.json printf 'registry=https://localhost:8443/\n//localhost:8443/:cafile=%s\n' /absolute/path/to/ca.pem > .npmrc pnpm install # succeeds pnpm audit signatures # fails
Error: ERR_PNPM_AUDIT_SIGNATURE_KEYS_FETCH_FAIL × Failed to request the registry keys endpoint (at https://localhost:8443/-/npm/v1/keys): │ error sending request for url (https://localhost:8443/-/npm/v1/keys)
Controls, all on 12.7.0 and 12.8.2:
- Scoping
cafileto the private CA plus the system roots (cat /etc/ssl/cert.pem ca.pem) makespnpm audit signaturespass. - Serving
/-/npm/v1/keysdirectly instead of redirecting makes it pass with the private CA alone. pnpm installwith a fresh store also succeeds when the registry redirects the tarball download to registry.npmjs.org, so installs are not affected.
Describe the Bug
Since 12.7.0, which honors custom CAs directly (#15329, #14461), a registry's per-registry TLS settings are applied to a cross-origin redirect of the audit signatures keys request. The registry answers <registry>/-/npm/v1/keys with a 302 to https://registry.npmjs.org/-/npm/v1/keys. pnpm follows it with the client built for //localhost:8443/, whose trust roots are the scoped cafile only (EffectiveTrustRoots::CustomOnly in pnpm/crates/network/src/client_builder.rs). npmjs's certificate therefore fails verification, even though no setting is scoped to registry.npmjs.org.
The request clients follow redirects with reqwest's default policy unless a redirect guard is set (apply_redirect_policy). Installs use ThrottledClient::new_for_installs_with_redirect_guard, which re-routes each hop, and redirected tarballs work. fetch_registry_keys in pnpm/crates/cli/src/cli_args/audit/signatures.rs uses the plain client, so the npmjs hop keeps the registry's TLS settings. #14323 describes the per-registry route and client structure involved.
Before 12.7.0 the scoped cafile was not used for this request at all, so a private-CA registry only worked when its CA was also in the system trust store. 12.7.0 fixed that, and the failure moved to the redirect hop:
| pnpm | Keys request reaches the private-CA registry | Result |
|---|---|---|
| 12.4.1 – 12.6.0 | no (scoped cafile not used) | ERR_PNPM_AUDIT_SIGNATURE_KEYS_FETCH_FAIL |
| 12.7.0 – 12.8.2 | yes, then 302 to npmjs | ERR_PNPM_AUDIT_SIGNATURE_KEYS_FETCH_FAIL |
The error names the original URL, not the redirect hop that failed TLS, which hides the cause.
Real-world impact: a self-managed GitLab npm registry behind a private CA. GitLab redirects its signing-keys endpoint to npmjs, so pnpm audit signatures fails with the registry's CA scoped as cafile. It passes only when that file also contains the public roots.
Expected Behavior
TLS settings scoped to //localhost:8443/ apply only to requests to that registry. When the keys request is redirected to https://registry.npmjs.org/, that hop should use the TLS settings for its own URL, here the default trust, the same way installs handle redirected tarballs. pnpm audit signatures then verifies the signatures. If a hop does fail, the error should name that hop's URL and the TLS cause.
Which Node.js version are you using?
24.18.0
Which operating systems have you used?
- macOS
- Windows
- Linux
If your OS is a Linux based, which one it is? (Include the version if relevant)
26.7.1, arm64
Contributing a fix
- I am working on a fix
Activity
fix(audit): use the redirect target's TLS settings for the signing ke…
fix(deps): update dependencias no-major (#23)