From c452019ac0d5e9855cae6a1ab6e73dfa8d77ee24 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Wed, 10 Jun 2026 08:01:14 +0200 Subject: [PATCH] fix(security): port the latest security fixes to v10 (#12300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(package-bins): reject reserved manifest bin names Manifest bin keys "", ".", "..", and scoped forms such as "@scope/.." passed the bin-name guard because encodeURIComponent leaves them unchanged. When joined to the global bin directory during global remove/update/add operations, "." resolves to the bin directory itself and ".." to its parent, which removeBin then recursively deletes. Reject empty, ".", and ".." bin names after scope stripping. Backport of pnpm/pnpm#12289 to v10. * fix: block untrusted request destination env expansion Makes environment expansion trust-aware for registry/auth config and request destinations: - Stops project and workspace .npmrc files from expanding ${...} placeholders in registry/proxy request destinations, URL-scoped keys, and registry credential values. - Stops repository-controlled pnpm-workspace.yaml from expanding ${...} placeholders in the registry setting. - Preserves env expansion for trusted user/global/CLI/env config so existing token and registry setup flows continue to work. Backport of pnpm/pnpm#12291 (CAND-PNPM-122 / GHSA-3qhv-2rgh-x77r) to v10. * fix(security): verify npm registry signature before spawning a package-manager binary The packageManager field (and pnpm self-update) makes pnpm download and run a specific pnpm version. The staged install's bytes were trusted based on lockfile integrity alone, which proves nothing when the inputs are repository-controlled. pnpm now verifies the npm registry signature of the engine it is about to spawn, over the installed integrity, against npm's public signing keys embedded in the pnpm CLI (exactly as corepack does): - verifyPnpmEngineIdentity() checks pnpm/@pnpm/exe and the materialized platform binaries of the staged install before it is linked into the tools directory. - Fails closed: any verification failure, including an unreachable registry, refuses the version switch rather than running an unverified binary. Runs only on a tools-directory cache miss (an actual download). - The embedded keys live in a generated file kept in sync with npm's keys endpoint by scripts/update-npm-signing-keys.mjs; the release workflow runs the check as a gate so a key rotation cannot silently break verification. Backport of pnpm/pnpm#12292 (CAND-PNPM-097) to v10. * fix: harden package-manager bootstrap metadata Resolve package-manager bootstrap traffic through trusted user/CLI registries and trusted network config, defaulting to the public npm registry instead of project/workspace registry settings: - getConfig() now computes packageManagerRegistries and packageManagerNetworkConfig from trusted config sources only (CLI options, env config, user and global .npmrc) — never the repository's project/workspace .npmrc or pnpm-workspace.yaml. - switchCliVersion() applies that bootstrap config when installing and verifying the wanted pnpm version, so repository .npmrc proxy/TLS/registry values cannot steer package-manager bootstrap traffic. Backport of pnpm/pnpm#12296 to v10. The v11 env-lockfile validation parts do not apply: v10 bootstraps the wanted version through a staged child install instead of an env lockfile. * fix(security): verify Node.js runtime SHASUMS OpenPGP signature When a repository requests a Node.js runtime (useNodeVersion or an execution env), pnpm downloads and then executes a Node binary. The download mirror is repository-configurable via node-mirror: in project .npmrc, and the integrity came from SHASUMS256.txt fetched from that same mirror — a circular check a malicious mirror can satisfy with a tampered binary and matching hashes. pnpm now fetches SHASUMS256.txt.sig and verifies its detached OpenPGP signature against the Node.js release team's public keys, embedded in the pnpm CLI, before trusting the hashes: - @pnpm/crypto.shasums-file: new fetchVerifiedNodeShasums / fetchVerifiedNodeShasumsFile verify the signature via openpgp against the embedded keys (generated src/nodeReleaseKeys.ts, mirrored from the canonical nodejs/release-keys list). - @pnpm/node.fetcher verifies the configurable-mirror SHASUMS for the release channel; pre-release channels (rc, nightly, ...) are unsigned by Node and remain unverified. - scripts/update-node-release-keys.mjs keeps the keys current (pnpm run check:node-release-keys / update:node-release-keys), and the release workflow runs the check as a gate. Backport of pnpm/pnpm#12295 to v10 (without the pacquet Rust port, which does not exist on this branch). * test(env): sign the SHASUMS fixture for Node.js download tests The Node.js download tests exercise the release channel, whose SHASUMS256.txt is now signature-verified. Sign the fixture with a generated OpenPGP key and trust it through the new trustedNodeReleaseKeys test seam (threaded from plugin-commands-env via @pnpm/node.fetcher to fetchVerifiedNodeShasums), so the tests keep exercising the verification path instead of bypassing it. * fix(self-updater): redact registry credentials from engine identity errors Registry URLs may legally embed basic-auth credentials (https://user:pass@host/). verifyPnpmEngineIdentity() interpolated the packument URL and registry URL into PnpmError messages, and the unreachable-registry path surfaced fetch-layer error messages that embed the request URL — all of which land in terminal output and CI logs. Strip URL credentials from every error message and truncate the non-200 response body. * fix: update vulnerable transitive dependencies Override shell-quote to >=1.8.4 (GHSA-w7jw-789q-3m8p, critical, pulled in via concurrently) so the audit workflow passes again. The advisory was published after the last release/10 audit run; it is unrelated to the security backports on this branch. --- .../clean-package-manager-registries.md | 6 + .changeset/pnpm-engine-identity.md | 6 + .changeset/sharp-registry-env-placeholders.md | 6 + .changeset/strange-bin-segments.md | 6 + .changeset/verify-node-runtime-shasums.md | 8 + .github/workflows/release.yml | 7 + config/config/src/Config.ts | 23 ++ .../config/src/dropUntrustedEnvExpansions.ts | 97 +++++ .../config/src/getOptionsFromRootManifest.ts | 11 + config/config/src/index.ts | 65 +++- .../test/getOptionsFromRootManifest.test.ts | 15 + config/config/test/index.ts | 273 +++++++++++++ crypto/shasums-file/package.json | 4 +- .../scripts/update-node-release-keys.mjs | 94 +++++ crypto/shasums-file/src/index.ts | 22 +- crypto/shasums-file/src/nodeReleaseKeys.ts | 122 ++++++ crypto/shasums-file/src/verifyNodeShasums.ts | 120 ++++++ crypto/shasums-file/test/verifyNodeShasums.ts | 66 ++++ cspell.json | 12 + env/node.fetcher/src/index.ts | 33 +- env/plugin-commands-env/package.json | 1 + .../src/downloadNodeVersion.ts | 3 +- env/plugin-commands-env/src/node.ts | 7 +- env/plugin-commands-env/test/node.test.ts | 37 +- package.json | 3 + pkg-manager/package-bins/src/index.ts | 8 +- pkg-manager/package-bins/test/index.ts | 20 + pnpm-lock.yaml | 177 ++++++--- pnpm-workspace.yaml | 3 + pnpm/package.json | 1 + pnpm/src/packageManagerRegistries.ts | 28 ++ pnpm/src/switchCliVersion.ts | 9 +- .../plugin-commands-self-updater/package.json | 6 + .../scripts/update-npm-signing-keys.mjs | 105 +++++ .../plugin-commands-self-updater/src/index.ts | 1 + .../src/installPnpmToTools.ts | 6 + .../src/npmSigningKeys.ts | 29 ++ .../src/selfUpdate.ts | 6 +- .../src/verifyPnpmEngineIdentity.ts | 362 ++++++++++++++++++ .../test/selfUpdate.test.ts | 3 + .../test/verifyPnpmEngineIdentity.test.ts | 227 +++++++++++ .../tsconfig.json | 18 + 42 files changed, 1988 insertions(+), 68 deletions(-) create mode 100644 .changeset/clean-package-manager-registries.md create mode 100644 .changeset/pnpm-engine-identity.md create mode 100644 .changeset/sharp-registry-env-placeholders.md create mode 100644 .changeset/strange-bin-segments.md create mode 100644 .changeset/verify-node-runtime-shasums.md create mode 100644 config/config/src/dropUntrustedEnvExpansions.ts create mode 100644 crypto/shasums-file/scripts/update-node-release-keys.mjs create mode 100644 crypto/shasums-file/src/nodeReleaseKeys.ts create mode 100644 crypto/shasums-file/src/verifyNodeShasums.ts create mode 100644 crypto/shasums-file/test/verifyNodeShasums.ts create mode 100644 pnpm/src/packageManagerRegistries.ts create mode 100644 tools/plugin-commands-self-updater/scripts/update-npm-signing-keys.mjs create mode 100644 tools/plugin-commands-self-updater/src/npmSigningKeys.ts create mode 100644 tools/plugin-commands-self-updater/src/verifyPnpmEngineIdentity.ts create mode 100644 tools/plugin-commands-self-updater/test/verifyPnpmEngineIdentity.test.ts diff --git a/.changeset/clean-package-manager-registries.md b/.changeset/clean-package-manager-registries.md new file mode 100644 index 0000000000..fd3fbd0b12 --- /dev/null +++ b/.changeset/clean-package-manager-registries.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config": patch +"pnpm": patch +--- + +Package-manager bootstrap traffic is now resolved through trusted registries and trusted network config. When pnpm downloads the pnpm version requested by a repository's `packageManager` field, the registry it fetches from (and the proxy/TLS settings used for that traffic) now come exclusively from trusted config sources — CLI options, env config, user and global `.npmrc` — defaulting to the public npm registry, instead of the repository's project/workspace settings. diff --git a/.changeset/pnpm-engine-identity.md b/.changeset/pnpm-engine-identity.md new file mode 100644 index 0000000000..82ce8a82dc --- /dev/null +++ b/.changeset/pnpm-engine-identity.md @@ -0,0 +1,6 @@ +--- +"@pnpm/tools.plugin-commands-self-updater": patch +"pnpm": patch +--- + +pnpm now verifies the npm registry signature of a package-manager binary before spawning it. When the `packageManager` field (or `pnpm self-update`) makes pnpm download another pnpm version, the staged install is verified corepack-style: the integrity recorded in the staged lockfile must carry a valid npm registry signature for the exact `name@version`, validated against npm's public signing keys that ship embedded in the pnpm CLI. Verification fails closed — a tampered download, an unsigned package, or an unreachable registry refuses the version switch rather than running an unverified binary. It runs only when the wanted version is actually downloaded (a tools-directory cache miss), so repeated commands pay no extra network round trip. diff --git a/.changeset/sharp-registry-env-placeholders.md b/.changeset/sharp-registry-env-placeholders.md new file mode 100644 index 0000000000..54581ec3aa --- /dev/null +++ b/.changeset/sharp-registry-env-placeholders.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config": patch +"pnpm": patch +--- + +Environment variable expansion is now trust-aware for registry/auth config and request destinations. Repository-controlled config files (the project and workspace `.npmrc` and `pnpm-workspace.yaml`) can no longer expand `${...}` placeholders in registry/proxy request destinations, URL-scoped keys, or registry credential values, preventing repository-controlled configuration from exfiltrating environment secrets through request URLs. Trusted user/global/CLI/env config keeps full env expansion, so existing token and registry setup flows continue to work. diff --git a/.changeset/strange-bin-segments.md b/.changeset/strange-bin-segments.md new file mode 100644 index 0000000000..88ba3f17de --- /dev/null +++ b/.changeset/strange-bin-segments.md @@ -0,0 +1,6 @@ +--- +"@pnpm/package-bins": patch +"pnpm": patch +--- + +Reject reserved manifest `bin` names (`""`, `"."`, `".."`, and scoped forms such as `@scope/..`) when resolving a package's bins. These names previously passed the bin-name guard and, when joined to the global bin directory during global remove/update/add operations, could resolve to the global bin directory itself or its parent and have it recursively deleted. diff --git a/.changeset/verify-node-runtime-shasums.md b/.changeset/verify-node-runtime-shasums.md new file mode 100644 index 0000000000..0a8bb73de2 --- /dev/null +++ b/.changeset/verify-node-runtime-shasums.md @@ -0,0 +1,8 @@ +--- +"@pnpm/crypto.shasums-file": patch +"@pnpm/node.fetcher": patch +"@pnpm/plugin-commands-env": patch +"pnpm": patch +--- + +pnpm now verifies the detached OpenPGP signature of a Node.js release's `SHASUMS256.txt` against the Node.js release team's public keys (embedded in the pnpm CLI) before trusting its hashes. The Node.js download mirror is repository-configurable (`node-mirror:` in `.npmrc`), and the integrity check previously trusted a `SHASUMS256.txt` fetched from that same mirror — a circular check that a malicious mirror could satisfy with a tampered binary and matching hashes. A mirror that proxies the real signed SHASUMS keeps working unchanged. Only the `release` channel publishes signed SHASUMS files, so pre-release channels (rc, nightly, …) remain unverified. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a915e660a..e3e3a01d64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,13 @@ jobs: - name: pnpm install # We use --force because we want all artifacts of @reflink/reflink to be installed. run: pnpm install --force + # Release gate: a stale embedded key set would make pnpm unable to verify + # packages signed with npm's rotated key, so block the release until the + # embedded keys are refreshed. + - name: Check embedded npm signing keys are in sync with the npm registry + run: pnpm run check:npm-signing-keys + - name: Check embedded Node.js release keys are in sync with nodejs/release-keys + run: pnpm run check:node-release-keys - name: Publish Packages env: # setting the "npm_config_//registry.npmjs.org/:_authToken" env variable directly doesn't work. diff --git a/config/config/src/Config.ts b/config/config/src/Config.ts index cac77229ed..d26eb3a74c 100644 --- a/config/config/src/Config.ts +++ b/config/config/src/Config.ts @@ -20,6 +20,27 @@ export interface WantedPackageManager { export type VerifyDepsBeforeRun = 'install' | 'warn' | 'error' | 'prompt' | false +/** + * Registry/network settings for package-manager bootstrap traffic + * (downloading the pnpm version requested by a repository's packageManager + * field). Built exclusively from trusted config sources — CLI options, env + * config, user and global .npmrc — never from the repository's project or + * workspace .npmrc, so a cloned repository cannot steer where the + * package-manager binary is fetched from or how that traffic is routed. + */ +export interface PackageManagerNetworkConfig { + ca?: string | string[] + cert?: string | string[] + httpProxy?: string + httpsProxy?: string + key?: string + localAddress?: string + noProxy?: string | boolean + rawConfig: Record + sslConfigs: Record + strictSsl?: boolean +} + export interface Config extends OptionsFromRootManifest { allProjects?: Project[] selectedProjectsGraph?: ProjectsGraph @@ -195,6 +216,8 @@ export interface Config extends OptionsFromRootManifest { blockExoticSubdeps?: boolean registries: Registries + packageManagerRegistries?: Registries + packageManagerNetworkConfig?: PackageManagerNetworkConfig sslConfigs: Record ignoreWorkspaceRootCheck: boolean workspaceRoot: boolean diff --git a/config/config/src/dropUntrustedEnvExpansions.ts b/config/config/src/dropUntrustedEnvExpansions.ts new file mode 100644 index 0000000000..7936736318 --- /dev/null +++ b/config/config/src/dropUntrustedEnvExpansions.ts @@ -0,0 +1,97 @@ +import util from 'util' +import { envReplace } from '@pnpm/config.env-replace' +import { readIniFileSync } from 'read-ini-file' + +const AUTH_VALUE_KEYS = ['_authToken', '_auth', '_password', 'username', 'tokenHelper', 'cert', 'key'] as const +const AUTH_VALUE_KEY_SUFFIXES = AUTH_VALUE_KEYS.map(key => `:${key}`) + +/** + * Removes from `source` (a parsed npm-conf source layer) every setting whose + * raw form in the `.npmrc` file at `filePath` uses a `${...}` placeholder in + * a request destination (registry/proxy URLs, URL-scoped keys) or in a + * registry credential value. Repository-controlled `.npmrc` files must not + * be able to expand environment variables into the URLs pnpm sends requests + * to or into the credentials attached to them. + */ +export function dropUntrustedEnvExpansions ( + source: Record, + filePath: string, + warnings: string[] +): void { + let raw: Record + try { + raw = readIniFileSync(filePath) as Record + } catch (err: unknown) { + if (util.types.isNativeError(err) && 'code' in err && (err.code === 'ENOENT' || err.code === 'EISDIR')) { + return + } + throw err + } + for (const [rawKey, rawValue] of Object.entries(raw)) { + let expandedKey = rawKey + if (hasEnvPlaceholder(rawKey)) { + try { + expandedKey = envReplace(rawKey, process.env) + } catch { + // npm-conf failed to expand this key too, so it never reached `source`. + continue + } + if (isRequestDestinationKey(rawKey) || isRequestDestinationKey(expandedKey)) { + deleteOwnSetting(source, expandedKey) + warnIgnoredRequestDestinationEnv(filePath, rawKey, warnings) + continue + } + if (isAuthValueKey(rawKey) || isAuthValueKey(expandedKey)) { + deleteOwnSetting(source, expandedKey) + warnIgnoredAuthValueEnv(filePath, rawKey, warnings) + continue + } + } + if (typeof rawValue === 'string' && hasEnvPlaceholder(rawValue)) { + if (isRequestDestinationValueKey(expandedKey)) { + deleteOwnSetting(source, expandedKey) + warnIgnoredRequestDestinationEnv(filePath, expandedKey, warnings) + continue + } + if (isAuthValueKey(expandedKey)) { + deleteOwnSetting(source, expandedKey) + warnIgnoredAuthValueEnv(filePath, expandedKey, warnings) + } + } + } +} + +function deleteOwnSetting (source: Record, key: string): void { + // npm-conf sources are chained via prototype, so only delete own keys. + if (Object.hasOwn(source, key)) { + delete source[key] + } +} + +function isRequestDestinationKey (key: string): boolean { + return isRegistryKey(key) || key.startsWith('//') +} + +function isRequestDestinationValueKey (key: string): boolean { + return isRegistryKey(key) || key === 'https-proxy' || key === 'http-proxy' || key === 'proxy' +} + +function isRegistryKey (key: string): boolean { + return key === 'registry' || (key.startsWith('@') && key.endsWith(':registry')) +} + +function isAuthValueKey (key: string): boolean { + return (AUTH_VALUE_KEYS as readonly string[]).includes(key) || AUTH_VALUE_KEY_SUFFIXES.some(suffix => key.endsWith(suffix)) +} + +export function hasEnvPlaceholder (value: string): boolean { + return /\$\{[^}]+\}/.test(value) +} + +function warnIgnoredRequestDestinationEnv (filePath: string, key: string, warnings: string[]): void { + warnings.push(`Ignored project-level request destination "${key}" in "${filePath}": environment variables are not expanded in repository-controlled registry or proxy URLs.`) +} + +function warnIgnoredAuthValueEnv (filePath: string, key: string, warnings: string[]): void { + warnings.push(`Ignored project-level auth setting "${key}" in "${filePath}": environment variables are not expanded in repository-controlled registry credentials.`) +} diff --git a/config/config/src/getOptionsFromRootManifest.ts b/config/config/src/getOptionsFromRootManifest.ts index f973d8266e..c20e3ec60d 100644 --- a/config/config/src/getOptionsFromRootManifest.ts +++ b/config/config/src/getOptionsFromRootManifest.ts @@ -116,11 +116,18 @@ export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: P return settings } +// Settings that name a request destination. pnpm-workspace.yaml and the +// root manifest are repository-controlled, so expanding ${...} placeholders +// in these values would let a repository exfiltrate environment variables +// through the URLs pnpm sends requests to. +const REQUEST_DESTINATION_SCALAR_KEYS = new Set(['registry']) + function replaceEnvInSettings (settings: PnpmSettings): PnpmSettings { const newSettings: PnpmSettings = {} for (const [key, value] of Object.entries(settings)) { const newKey = envReplace(key, process.env) if (typeof value === 'string') { + if (REQUEST_DESTINATION_SCALAR_KEYS.has(newKey) && hasEnvPlaceholder(value)) continue // @ts-expect-error newSettings[newKey as keyof PnpmSettings] = envReplace(value, process.env) } else { @@ -130,6 +137,10 @@ function replaceEnvInSettings (settings: PnpmSettings): PnpmSettings { return newSettings } +function hasEnvPlaceholder (value: string): boolean { + return /\$\{[^}]+\}/.test(value) +} + function createVersionReferencesReplacer (manifest: ProjectManifest): (spec: string) => string { const allDeps = { ...manifest.devDependencies, diff --git a/config/config/src/index.ts b/config/config/src/index.ts index c566b9e003..e2dc0a20c4 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -20,6 +20,7 @@ import pathAbsolute from 'path-absolute' import which from 'which' import { inheritAuthConfig } from './auth.js' import { checkGlobalBinDir } from './checkGlobalBinDir.js' +import { dropUntrustedEnvExpansions } from './dropUntrustedEnvExpansions.js' import { rescopeUnscopedCreds } from './rescopeUnscopedCreds.js' import { hasDependencyBuildOptions, extractAndRemoveDependencyBuildOptions } from './dependencyBuildOptions.js' import { getNetworkConfigs } from './getNetworkConfigs.js' @@ -27,6 +28,7 @@ import { transformPathKeys } from './transformPath.js' import { getCacheDir, getConfigDir, getDataDir, getStateDir } from './dirs.js' import { type Config, + type PackageManagerNetworkConfig, type ConfigWithDeprecatedSettings, type UniversalOptions, type VerifyDepsBeforeRun, @@ -47,7 +49,7 @@ export { getOptionsFromRootManifest, getOptionsFromPnpmSettings, type OptionsFro export * from './readLocalConfig.js' export { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js' -export type { Config, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun } +export type { Config, PackageManagerNetworkConfig, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun } type CamelToKebabCase = S extends `${infer T}${infer U}` ? `${T extends Capitalize ? '-' : ''}${Lowercase}${CamelToKebabCase}` @@ -238,6 +240,19 @@ export async function getConfig (opts: { if (warn) warnings.push(warn) } + // The project and workspace .npmrc files are repository-controlled, so + // they must not be able to expand environment variables into request + // destinations (registry/proxy URLs, URL-scoped keys) or registry + // credential values. Settings whose raw form uses a ${...} placeholder in + // such a position are dropped from the source before the merged config is + // built. Trusted sources (cli, env, user, global, builtin) keep full env + // expansion. + for (const name of ['project', 'workspace']) { + const sourceEntry = npmConfig.sources[name] as { path?: string, data?: Record } | undefined + if (sourceEntry?.path == null || sourceEntry.data == null) continue + dropUntrustedEnvExpansions(sourceEntry.data, sourceEntry.path, warnings) + } + // After every source (cli, env, project, workspace, user, global, // pnpm-global, pnpm-builtin, npm-builtin) has been loaded, rewrite each // source's unscoped per-registry credential/cert keys to their URL-scoped @@ -513,6 +528,48 @@ export async function getConfig (opts: { // @ts-expect-error pnpmConfig.noProxy = pnpmConfig['noproxy'] ?? getProcessEnv('no_proxy') } + { + // Package-manager bootstrap traffic (downloading the pnpm version + // requested by the repository's packageManager field) must not be steered + // by repository-controlled settings. Build the bootstrap registry/network + // config from trusted sources only: CLI options, env config, user and + // global .npmrc — never the project/workspace .npmrc (pnpm-workspace.yaml + // settings are also excluded because they were applied to pnpmConfig, not + // to the npm-conf source chain re-merged here). + const untrustedData = new Set( + ['project', 'workspace'] + .map((name) => (npmConfig.sources[name] as { data?: Record } | undefined)?.data) + .filter(Boolean) + ) + const trustedRawConfig: Record = Object.assign.apply(Object, [ + {}, + ...[...npmConfig.list].reverse().filter((data: Record) => !untrustedData.has(data)), + cliOptions, + ] as any) // eslint-disable-line @typescript-eslint/no-explicit-any + const trustedNetworkConfigs = getNetworkConfigs(trustedRawConfig as Record) + pnpmConfig.packageManagerRegistries = { + default: typeof trustedRawConfig.registry === 'string' && trustedRawConfig.registry !== '' + ? normalizeRegistryUrl(trustedRawConfig.registry) + : 'https://registry.npmjs.org/', + ...trustedNetworkConfigs.registries, + } + const trustedHttpsProxy = getProxyValue( + trustedRawConfig['https-proxy'] ?? trustedRawConfig.proxy, + getProcessEnv('https_proxy') + ) + pnpmConfig.packageManagerNetworkConfig = { + ca: trustedRawConfig.ca as string | string[] | undefined, + cert: trustedRawConfig.cert as string | string[] | undefined, + httpProxy: trustedHttpsProxy ?? getProcessEnv('http_proxy') ?? getProcessEnv('proxy'), + httpsProxy: trustedHttpsProxy, + key: trustedRawConfig.key as string | undefined, + localAddress: trustedRawConfig['local-address'] as string | undefined, + noProxy: (trustedRawConfig.noproxy ?? getProcessEnv('no_proxy')) as string | boolean | undefined, + rawConfig: trustedRawConfig as Record, + sslConfigs: trustedNetworkConfigs.sslConfigs, + strictSsl: trustedRawConfig['strict-ssl'] as boolean | undefined, + } + } switch (pnpmConfig.nodeLinker) { case 'pnp': pnpmConfig.enablePnp = pnpmConfig.nodeLinker === 'pnp' @@ -582,6 +639,12 @@ export async function getConfig (opts: { return { config: pnpmConfig, warnings } } +function getProxyValue (value: unknown, fallback: string | undefined): string | undefined { + if (value === false || value === null) return undefined + if (typeof value === 'string' && value.length > 0) return value + return fallback +} + function getProcessEnv (env: string): string | undefined { return process.env[env] ?? process.env[env.toUpperCase()] ?? diff --git a/config/config/test/getOptionsFromRootManifest.test.ts b/config/config/test/getOptionsFromRootManifest.test.ts index edb2645c94..65de8a49ac 100644 --- a/config/config/test/getOptionsFromRootManifest.test.ts +++ b/config/config/test/getOptionsFromRootManifest.test.ts @@ -175,6 +175,21 @@ test('getOptionsFromPnpmSettings() replaces env variables in settings', () => { expect(options.foo).toEqual('bar') }) +test('getOptionsFromPnpmSettings() ignores env variables inside registry setting', () => { + process.env.PNPM_TEST_HOST = 'registry.example.com' + const options = getOptionsFromPnpmSettings(process.cwd(), { + registry: 'https://${PNPM_TEST_HOST}/npm/', // eslint-disable-line + } as any) as any // eslint-disable-line + expect(options.registry).toBeUndefined() +}) + +test('getOptionsFromPnpmSettings() keeps a registry setting without env placeholders', () => { + const options = getOptionsFromPnpmSettings(process.cwd(), { + registry: 'https://registry.example.com/npm/', + } as any) as any // eslint-disable-line + expect(options.registry).toBe('https://registry.example.com/npm/') +}) + test('getOptionsFromRootManifest() converts allowBuilds', () => { const options = getOptionsFromRootManifest(process.cwd(), { pnpm: { diff --git a/config/config/test/index.ts b/config/config/test/index.ts index ea63c1bac6..39da6ca6e8 100644 --- a/config/config/test/index.ts +++ b/config/config/test/index.ts @@ -238,6 +238,279 @@ test('registries in current directory\'s .npmrc have bigger priority then global }) }) +/* eslint-disable no-template-curly-in-string */ +describe('project .npmrc env expansion trust boundary', () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = { + ...originalEnv, + PNPM_TEST_TOKEN: 'secret', + } + }) + + afterEach(() => { + process.env = originalEnv + }) + + test('project .npmrc does not expand env variables in registry URLs', async () => { + prepare() + + fs.writeFileSync('.npmrc', 'registry=https://registry.example.com/${PNPM_TEST_TOKEN}/\n', 'utf8') + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8') + + const { config, warnings } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(config.registries.default).not.toBe('https://registry.example.com/secret/') + expect(JSON.stringify(config.registries)).not.toContain('secret') + expect(warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Ignored project-level request destination "registry"'), + ])) + }) + + test('project .npmrc does not expand env variables in scoped registry URLs or URL-scoped keys', async () => { + prepare() + + fs.writeFileSync('.npmrc', [ + '@scope:registry=https://registry.example.com/${PNPM_TEST_TOKEN}/', + '//registry.example.com/${PNPM_TEST_TOKEN}/:_authToken=token', + '', + ].join('\n'), 'utf8') + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8') + + const { config, warnings } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(config.registries['@scope']).toBeUndefined() + expect(JSON.stringify(config.rawConfig)).not.toContain('secret') + expect(warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Ignored project-level request destination "@scope:registry"'), + expect.stringContaining('Ignored project-level request destination "//registry.example.com/${PNPM_TEST_TOKEN}/:_authToken"'), + ])) + }) + + test('project .npmrc does not expand env variables in auth values', async () => { + prepare() + + process.env.PNPM_TEST_CERT = 'secret-cert' + process.env.PNPM_TEST_KEY = 'secret-key' + process.env.PNPM_TEST_PASSWORD = Buffer.from('secret').toString('base64') + process.env.PNPM_TEST_USER = 'secret-user' + + fs.writeFileSync('.npmrc', [ + 'registry=https://attacker.example/', + '//attacker.example/:_authToken=${PNPM_TEST_TOKEN}', + '//attacker.example/:cert=${PNPM_TEST_CERT}', + '//attacker.example/:key=${PNPM_TEST_KEY}', + '_authToken=${PNPM_TEST_TOKEN}', + 'username=${PNPM_TEST_USER}', + '_password=${PNPM_TEST_PASSWORD}', + 'cert=${PNPM_TEST_CERT}', + 'key=${PNPM_TEST_KEY}', + '', + ].join('\n'), 'utf8') + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8') + + const { config, warnings } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + const serializedRawConfig = JSON.stringify(config.rawConfig) + expect(serializedRawConfig).not.toContain('secret-token') + expect(serializedRawConfig).not.toContain('secret-user') + expect(serializedRawConfig).not.toContain('secret-cert') + expect(serializedRawConfig).not.toContain('secret-key') + expect(warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Ignored project-level auth setting "//attacker.example/:_authToken"'), + expect.stringContaining('Ignored project-level auth setting "//attacker.example/:cert"'), + expect.stringContaining('Ignored project-level auth setting "//attacker.example/:key"'), + expect.stringContaining('Ignored project-level auth setting "_authToken"'), + expect.stringContaining('Ignored project-level auth setting "username"'), + expect.stringContaining('Ignored project-level auth setting "_password"'), + expect.stringContaining('Ignored project-level auth setting "cert"'), + expect.stringContaining('Ignored project-level auth setting "key"'), + ])) + }) + + test('project .npmrc does not expand env variables in proxy URLs', async () => { + prepare() + + fs.writeFileSync('.npmrc', [ + 'https-proxy=http://proxy.example.com/${PNPM_TEST_TOKEN}/', + 'http-proxy=http://proxy.example.com/${PNPM_TEST_TOKEN}/', + 'proxy=http://legacy-proxy.example.com/${PNPM_TEST_TOKEN}/', + '', + ].join('\n'), 'utf8') + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8') + + const { config, warnings } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(config.httpsProxy).toBeUndefined() + expect(config.httpProxy).toBeUndefined() + expect(JSON.stringify(config.rawConfig)).not.toContain('proxy.example.com/secret') + expect(warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Ignored project-level request destination "https-proxy"'), + expect.stringContaining('Ignored project-level request destination "http-proxy"'), + expect.stringContaining('Ignored project-level request destination "proxy"'), + ])) + }) + + test('workspace .npmrc does not expand env variables in registry URLs', async () => { + prepare() + + fs.writeFileSync('.npmrc', '', 'utf8') + fs.mkdirSync('workspace-root') + fs.writeFileSync(path.resolve('workspace-root', '.npmrc'), 'registry=https://registry.example.com/${PNPM_TEST_TOKEN}/\n', 'utf8') + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8') + + const { config, warnings } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + workspaceDir: path.resolve('workspace-root'), + }) + + expect(JSON.stringify(config.registries)).not.toContain('secret') + expect(warnings).toEqual(expect.arrayContaining([ + expect.stringContaining('Ignored project-level request destination "registry"'), + ])) + }) + + test('user .npmrc may expand env variables in registry URLs', async () => { + prepare() + + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), 'registry=https://registry.example.com/${PNPM_TEST_TOKEN}/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(config.registries.default).toBe('https://registry.example.com/secret/') + }) +}) + +test('pnpm-workspace.yaml registry setting does not expand env variables', async () => { + const originalEnv = process.env + process.env = { ...originalEnv, PNPM_TEST_TOKEN: 'secret' } + try { + prepareEmpty() + + fs.writeFileSync('pnpm-workspace.yaml', 'registry: https://private.example.com/${PNPM_TEST_TOKEN}/\n', 'utf8') + fs.mkdirSync('user-home') + fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8') + + const { config } = await getConfig({ + cliOptions: { + userconfig: path.resolve('user-home', '.npmrc'), + }, + packageManager: { name: 'pnpm', version: '1.0.0' }, + workspaceDir: process.cwd(), + }) + + expect(config.registry).not.toBe('https://private.example.com/secret/') + expect(JSON.stringify(config.registries)).not.toContain('secret') + } finally { + process.env = originalEnv + } +}) +/* eslint-enable no-template-curly-in-string */ + +test('packageManagerRegistries is built from trusted config only', async () => { + prepare() + + fs.writeFileSync('.npmrc', [ + 'registry=https://repo-registry.example.test/', + '@scope:registry=https://repo-scope.example.test/', + '', + ].join('\n'), 'utf8') + fs.mkdirSync('user-home') + const userconfig = path.resolve('user-home', '.npmrc') + fs.writeFileSync(userconfig, 'registry=https://user-registry.example.test/\n', 'utf8') + + const { config } = await getConfig({ + cliOptions: { + userconfig, + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + // The merged registries follow the repository config, but the + // package-manager bootstrap registries must not. + expect(config.registries.default).toBe('https://repo-registry.example.test/') + expect(config.packageManagerRegistries?.default).toBe('https://user-registry.example.test/') + expect(config.packageManagerRegistries?.['@scope']).toBeUndefined() + expect(config.packageManagerNetworkConfig?.rawConfig.registry).toBe('https://user-registry.example.test/') +}) + +test('packageManagerRegistries defaults to the public npm registry', async () => { + prepare() + + fs.writeFileSync('.npmrc', 'registry=https://repo-registry.example.test/\n', 'utf8') + fs.mkdirSync('user-home') + const userconfig = path.resolve('user-home', '.npmrc') + fs.writeFileSync(userconfig, '', 'utf8') + + const { config } = await getConfig({ + cliOptions: { + userconfig, + }, + packageManager: { + name: 'pnpm', + version: '1.0.0', + }, + }) + + expect(config.packageManagerRegistries?.default).toBe('https://registry.npmjs.org/') +}) + test('filter is read from .npmrc as an array', async () => { prepareEmpty() diff --git a/crypto/shasums-file/package.json b/crypto/shasums-file/package.json index e66a61bb18..292665fd87 100644 --- a/crypto/shasums-file/package.json +++ b/crypto/shasums-file/package.json @@ -35,9 +35,11 @@ "dependencies": { "@pnpm/crypto.hash": "workspace:*", "@pnpm/error": "workspace:*", - "@pnpm/fetching-types": "workspace:*" + "@pnpm/fetching-types": "workspace:*", + "openpgp": "catalog:" }, "devDependencies": { + "@openpgp/web-stream-tools": "catalog:", "@pnpm/crypto.shasums-file": "workspace:*" }, "engines": { diff --git a/crypto/shasums-file/scripts/update-node-release-keys.mjs b/crypto/shasums-file/scripts/update-node-release-keys.mjs new file mode 100644 index 0000000000..15adf46d9c --- /dev/null +++ b/crypto/shasums-file/scripts/update-node-release-keys.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node +// Mirrors the Node.js release team's OpenPGP public keys (used to verify the +// signature of SHASUMS256.txt) from the canonical nodejs/release-keys repo into +// src/nodeReleaseKeys.ts. +// +// node update-node-release-keys.mjs # check (CI / release gate) +// node update-node-release-keys.mjs --update # rewrite the embedded keys +// +// `--check` fails when the authoritative keys.list contains a fingerprint that +// is not embedded, so a newly added release signer cannot silently break Node +// runtime verification. +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const RAW = 'https://raw.githubusercontent.com/nodejs/release-keys/main' +const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..') +const TS_KEYS_FILE = path.join(ROOT, 'crypto', 'shasums-file', 'src', 'nodeReleaseKeys.ts') + +async function main () { + const update = process.argv.includes('--update') + const fingerprints = (await (await fetchOk(`${RAW}/keys.list`)).text()) + .split('\n').map((l) => l.trim()).filter(Boolean) + + const embedded = [ + readEmbeddedFingerprints(TS_KEYS_FILE, /fingerprint: '([0-9A-F]+)'/g), + ] + const missing = embedded.flatMap(({ label, fingerprints: embeddedFingerprints }) => + fingerprints.filter((fp) => !embeddedFingerprints.includes(fp)).map((fp) => ({ label, fp })) + ) + // Keys embedded here but no longer in the canonical list (e.g. revoked/rotated) + // must NOT stay in the trust set, so they fail the check too. + const extra = embedded.flatMap(({ label, fingerprints: embeddedFingerprints }) => + embeddedFingerprints.filter((fp) => !fingerprints.includes(fp)).map((fp) => ({ label, fp })) + ) + + if (!update) { + if (missing.length === 0 && extra.length === 0) { + console.log(`✓ Embedded Node.js release keys are up to date (${fingerprints.length} key(s)).`) + return + } + console.error('✗ Embedded Node.js release keys are out of sync with nodejs/release-keys.') + if (missing.length > 0) console.error(` Missing (add): ${formatDrift(missing)}`) + if (extra.length > 0) console.error(` No longer canonical (remove — possibly revoked): ${formatDrift(extra)}`) + console.error(`Run: node ${path.relative(process.cwd(), fileURLToPath(import.meta.url))} --update`) + process.exit(1) + } + + const keys = [] + for (const fp of fingerprints) { + const armored = (await (await fetchOk(`${RAW}/keys/${fp}.asc`)).text()).trim() + keys.push({ fingerprint: fp, armored }) + } + fs.writeFileSync(TS_KEYS_FILE, renderTypeScript(keys)) + console.log(`✓ Wrote ${keys.length} Node.js release key(s).`) +} + +async function fetchOk (url) { + const res = await fetch(url) + if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`) + return res +} + +function readEmbeddedFingerprints (file, pattern) { + const label = path.relative(ROOT, file) + if (!fs.existsSync(file)) return { label, fingerprints: [] } + return { + label, + fingerprints: [...fs.readFileSync(file, 'utf8').matchAll(pattern)].map((m) => m[1]), + } +} + +function formatDrift (items) { + return items.map(({ label, fp }) => `${label}: ${fp}`).join(', ') +} + +function renderTypeScript (keys) { + const entries = keys.map(({ fingerprint, armored }) => + ` {\n fingerprint: '${fingerprint}',\n armoredKey: ${JSON.stringify(`${armored}\n`)},\n },`).join('\n') + return `/* eslint-disable */ +// cspell:disable +// GENERATED — the Node.js release team's OpenPGP public keys, mirrored from +// (keys.list + keys/.asc). +// +// Used to verify the signature of a Node.js release's SHASUMS256.txt before +// trusting its hashes. Refresh with: +// node crypto/shasums-file/scripts/update-node-release-keys.mjs --update +export const NODE_RELEASE_KEYS = [ +${entries} +] as const satisfies ReadonlyArray<{ fingerprint: string, armoredKey: string }> +` +} + +main().catch((err) => { console.error(err); process.exit(1) }) diff --git a/crypto/shasums-file/src/index.ts b/crypto/shasums-file/src/index.ts index a3ec58e31e..9fe8f872e0 100644 --- a/crypto/shasums-file/src/index.ts +++ b/crypto/shasums-file/src/index.ts @@ -3,6 +3,10 @@ import { type FetchFromRegistry, } from '@pnpm/fetching-types' +import { fetchVerifiedNodeShasums } from './verifyNodeShasums.js' + +export { fetchVerifiedNodeShasums, type ArmoredKey } from './verifyNodeShasums.js' + export interface ShasumsFileItem { integrity: string fileName: string @@ -12,7 +16,23 @@ export async function fetchShasumsFile ( fetch: FetchFromRegistry, shasumsUrl: string ): Promise { - const shasumsFileContent = await fetchShasumsFileRaw(fetch, shasumsUrl) + return parseShasumsFile(await fetchShasumsFileRaw(fetch, shasumsUrl)) +} + +/** + * Like {@link fetchShasumsFile}, but first verifies the SHASUMS file's detached + * OpenPGP signature against the Node.js release keys (see + * {@link fetchVerifiedNodeShasums}). Use this whenever the SHASUMS file is + * fetched from a repository-configurable Node.js mirror. + */ +export async function fetchVerifiedNodeShasumsFile ( + fetch: FetchFromRegistry, + shasumsUrl: string +): Promise { + return parseShasumsFile(await fetchVerifiedNodeShasums(fetch, shasumsUrl)) +} + +export function parseShasumsFile (shasumsFileContent: string): ShasumsFileItem[] { const lines = shasumsFileContent.split('\n') const items: ShasumsFileItem[] = [] for (const line of lines) { diff --git a/crypto/shasums-file/src/nodeReleaseKeys.ts b/crypto/shasums-file/src/nodeReleaseKeys.ts new file mode 100644 index 0000000000..43967ae5fe --- /dev/null +++ b/crypto/shasums-file/src/nodeReleaseKeys.ts @@ -0,0 +1,122 @@ +/* eslint-disable */ +// cspell:disable +// GENERATED — the Node.js release team's OpenPGP public keys, mirrored from +// (keys.list + keys/.asc). +// +// Used to verify the signature of a Node.js release's SHASUMS256.txt before +// trusting its hashes. Refresh with: +// node crypto/shasums-file/scripts/update-node-release-keys.mjs --update +export const NODE_RELEASE_KEYS = [ + { + fingerprint: '4ED778F539E3634C779C87C6D7062848A1AB005C', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nMH8=\n=H626\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '94AE36675C464D64BAFA68DD7434390BDBE9B9C5', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nmMJZL+1dR71q\n=sm/t\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '1C050899334244A8AF75E53792EF661D867B9DFA', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=ZsQu\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'B9AE9905FFD7803F25714661B63B535A4C206CA9', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=CMP0\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '77984A986EBC2AA786BC0F66B01FBB92821C587A', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nKoZmFSI=\n=Fwcs\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '71DCFD284A79C3B38668286BC97EC7A07EDE3FC1', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nntI6COSRy17o3GzXm9d4V70=\n=h9up\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '61FC681DFB92A079F1685E77973F295594EC4689', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n+SMSBsOPDf0=\n=i++9\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'FD3A5288F042B6850C66B31F09FE44734EB7990E', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=NfOh\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFcGZx4BEACa92SjEniMQIBdb0btnZRu8vzOGNe+ndzXIWPyu2h+p0xZ/2JN\nMDQW5hc8USoV4/rTssdqDOqcu3AkmLtZi14IaRJ1TQP6Zb05I8MOEm58WXXn7fSF\nYJwhD3LDrAdAHAs896QvsFG7X3Rw18+j7RpK/MPIXZDA5GS3QPfrB67q/J3vvJyQ\neNz9jSlnMpkNO3KQYvUuU1KqeBpMXZtJi52B6FQY7y3H27MgjmJ2EEX9f1uNaxUw\n0SzHCJhKXFjAoeKIwrE/MwcbSks2Ax8lHMlLAgaio77nfdvrEtHbXUIbOGlY7gT/\nPzwav9ofCE3thvfTAzcScIENgmRuun2PEItxAO2ysqzpnj8cbdknF+ZVQohpGV1d\n2ECyYLcwQLWiJd+UR/rr0IJ8KvJI0dMxZxul055JF4UqU0O98BsRABi5GiIg6zgn\nPZm2Tr6Um90rPjKcVgJ3DgxeGkeYjvfxEj4pX4muZkouJdi4BGnvUB/pkKFaf9pl\nyx/hioMMF4tywify74+avPseZDaXRJSxqz+uXEy8VApN263oIiJAWiXWwPunTaWP\nnMBoAZcJm+2im7NwB8x3jxUfK9M8GcsOOUT+grpe0OguLwH1vhYSaLg9rlpjMBdb\n3xF6X7N6/h8PaHGUatHrvht+0V6NchmtdTVnJzOXOsI6rBBFc6ON2Yz7RQARAQAB\ntC9NaWNoYcOrbCBaYXNzbyAoVGFyZ29zKSA8dGFyZ29zQHByb3Rvbm1haWwuY29t\nPokCVQQTAQgAPwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AWIQSPzKE/7x0M\nLpEAjgl3D3qaWuFWAAUCaTgfOQUJJP27GwAKCRB3D3qaWuFWAOH+D/4lJ0ldFrsm\npt/w04XNgwRajkv1LniHMmzAss1TerrgrWALMh8A8rhxkAxYekcAYghGQEXomtNO\nj2hBe95DmQLhFob7INy6uaoRXcLPdHYm7K7zkKjOpMD7Hqni+ozmU53zrI/Myj+A\nE3kFP199JnL8Bj2fnJLHVTsdTxSERuUkLuVmMHjjqKRrbmSn7x9uysXhF6yFD/Wt\nML8A1pFbTn04VO+pLNK8mxoH+TJ1g4N8Zi8B0qzvCTBTkNhPrKztNlQWmRuvWiZ3\n7GADTGdanNbuiM9sUt/vY4cz5lRDZybO8Xk1MXFJ6ISrjZWPMbt6fF40HTqj9d+c\n0BpWJKulOUWh8Bcna4HFv/EKODoVteSshDnixqNgkOXhooM87TnBh/1hivLLVj5j\nKk54VQ44P/f88IOs+TaUCxPgj6+ww6+dWpxyM5S925Gu6EbVDUnkjPWPQHfcVUih\nKnA1pTLkqnA1DyJikiyBDXeGpPtyqibIHFnLS+DcHwc/dhnXUMfzOETUf2VnwkmD\nxBoQmIf8g6o3ggmhIAIzcB5MxI7RBteLnM8D80P3X1eeRPwIJaRC0zL9WIuUtuON\ngCr/SXMkyBBQCxluMVVC2U+xH2Y4QnGXIkjZJzhx/XdOCg2MFe48S8bxT4HU24Tn\nMmshf7gzMKuWs29Fr1NNXc6okhP8RrL2ybkCDQRXBmceARAAysxKPzZLnWG+QZUr\naQHUoYndRG5Y0toYHvCk9mUm98+aRvxUyG9VwRTkQzWv2e2yL1kX+Gs36c47XZNo\nOeOwfkDtv7QXDVp/7h2LSFLaXYg+We0EXdNAm5yjJRq7dEIziJgJQOcL9VC6jjWa\nfk578uMZOyQWdcAmcuzT9aAzdz1nbqVK3gBOj1pDSIK4OxiI5bgsLN2SE8vreaFQ\nqXEatBw5Aik5tU00Suv/B7T8oYi27/JZt7f9+m4cSAlrChyRasF9ALyotQBpQarj\ncWuYevc6cmLsh4d5p15tDlRChu9uwHAZ1mZzruZK8vgeWgdsIZ1oyDR907u8kqOh\nUBC5f7VDkw8tWdokhAraNGYs4SRYCR28myxVSTBl2j1/uWXOxBXNjojMql17bMJs\nbli+ajCNBJGuOhgB/m75DT2Mt6rcuDE2lc4yQzih7C7f46caEy8k9kmHMDLYNtOc\nJmTmQ3dkaOTdKTcRE1EzyWdRLEWeXvDO2X5oZT0wYygppJxPEaLU+ChWrU9hLdpd\ne+fsSyQMqguX30hLhXz+HDsQqJF3LfQs7tCH6nDx5UqrVNIEizBnuS/DolXmgDOd\noczZUgeGtS1gWU0jfDX1KEdoeSQJz87hcA4FesDryeQpiyTAdwe9JlMI86K4ceGw\nksXl9LeVNJn84CgcfPKuIy2+sGMAEQEAAYkCPAQYAQgAJgIbDBYhBI/MoT/vHQwu\nkQCOCXcPeppa4VYABQJpOB9kBQkUEuvGAAoJEHcPeppa4VYAXckP/322cQ3GTB9x\nPGndr7kbQrEE5z8fhatMVk9YlamujoWYRQzXWjmhK5wHuUIQur3sGspuAGLPSUfn\nLK1r6qPGp719vSS0MVco4ME/qR98txo47/Mjc8uBjwWpbODf0AjEjwBN5jauXdi0\nw4hu68+QfbffAqIu6libBfitjHrNT7/DnwTLhDvlHpLqkKgQutvJ1Sf1GBQ9iDbo\nIu6hd+7QAzvW50FZE887gwOBha6F01EJLxqy1cwmQkT1QHCiLPHv/N9oIX2uYsQD\nUtW+yuRkG06E5at0fYrQNLksQMmsVU8MTxNMYJk7Ac9gTn9den71uQzt2QxaYBhA\nQLkAMUweFzBPlNU3IDirpYsz8+0TT0jsAIgNGJ/r09pDpgyDsnyuwVqBDLj7zWU3\nCaNfn6Es6/Z57fjnd8OD5iC0J/2ykURnhz1RHL8SSikOmkLaZQps4SfpF7PnuGHm\nry4fNFZrt0YeiOLyucBAbHMi02UpGd51yRvFJ9EK63JR5/orjsH1vw7gI1In93zs\n23SWnx8JL5l1uGlkJ/PN8LJJI751FeezE88wD0aOG3C+sMvuZJdahOHlGDEm7xKR\nBA+da9/uNqbKAfMzX3K3eto9RoeBQeLqgYouT85gHmZLeD7kYxI9NnU9pxeeELmF\nreo3MNn8Ye0sXg12rQIxV/isDIk6zg6GuDMEaSAoCRYJKwYBBAHaRw8BAQdAq/Y6\n0uQAJXs+LO3yhZJtKRbow22NHbOKOsY/BMFFnOuJArMEGAEIACYCGwIWIQSPzKE/\n7x0MLpEAjgl3D3qaWuFWAAUCaTgfZAUJAfkq2wCBdiAEGRYKAB0WIQSGyNdGQuZ4\nRvjhIChNqoDR5ze8nwUCaSAoCQAKCRBNqoDR5ze8nxryAP9IiA8ZTV6GQ/kpwLJq\n+87o+HZTojSYtN/ZfDQrPuHHFAD/dkGOXiutQcEM1YKDTTTSVQcdFhPFLChIGtoh\nExpmYgQJEHcPeppa4VYAp54P/1Dk/wLnuCB7XjNeIEA9UdU1w6M+cpWuzSlaLkjf\ngL95r89/SG3n670qNh54mTz0Uod7QV9BijoxNopbz2daPNJwf+WYlxk3pmCngXwu\nck8efXwgl6C1NrnZiJBvtQO6qXabmqTs59PumLjoR0i43knw8iaYQYdnuKBFJ6K6\n5xpMoYJAzj1nxHAESPhcbTe+nRPnGrfVwQ2qvK7iSL6eU80emHOWsxPaFo6SA9no\nDvzWmSwZ6jmFJo43lasrkLW8QH+8OdsORc7nBm1J7hXk24KAIHOrxBm5Icpc89XF\nxCJNI8fEP/WZJNfEx61m25XWIDuPz4mTgCaBHvrp+6bvXuVC8QiEFhqYGPpklN8G\n/cYG9OyDPOyTSPjfoUPYQwNap+SPxzqYIg/is9lqeIsMEV7Sbg8FLGlhP52sxnxR\ntrA7kZMfXFphCC42PMojJEBq/nlmXC79G64Q3kbmmDZ5kbzmYBtoPSEou17MGGCi\nwicnOtUO/XHct5/Wfkq2z5jYuj5Wbe5DNBV/eWp43uMpxNjkmfj+SdJtpI9MJ5B2\nUwJ09JwwPNF5B1RasQnXDLImINEoLOxixNddwk+2hZn5p7YwbZ02MznDaWVuiX86\nVpZxqzy2y5PDphbf/Rbx4ePeziYIsa0PRyjrS6fPu5QUV/EBK2V6nibMHsGuSfin\nR/5R\n=+ahi\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nftyH6K7HWwkSmrXf0//7kPEF7+P0LvSX4Ri/X2zfa+GgkfmIK66n8Vl/+YXSZxr8\n3Yf06k16Nl0loYZqYYkhQvdxKIyrXKx4zLiIU3QOKWFUy5EvAFwg++i5s8YvSvxb\n72p5tlhZ7C2I5/zzZG6+VPFEa4kCOQQTAQgAIwUCWJ4DrwIbAwcLCQgHAwIBBhUI\nAgkKCwQWAgMBAh4BAheAAAoJEOc7xkHMEfTI3RUQAKagnz10ZI0WUXYbQlZ09eUG\nzDzxA8gpsMAoEb31+vo3bwFJ8hlErSkWe/WsHoaF7y0BiqWlUYibdpKrdmNnivbs\nj5TTb5MEndP/kKYsfKQZtbNZwP5ITU3lKVEPHCBZbDYQWKPdQBSq/6qvQ1D1/lSr\n6VEhML0p/XbFy9st22jMqTWiSEu6xHyOf7t6+niLcLuBo/edR/+jOEF8qizGYZaU\nR7eFtRkWD5I8y8sPWtjjiRmbnV/1YcJNfZKDNu4ReVzXh9iJBt3iC8HY1nUFo2ic\n2bZ31odwpGLrfebHtL63jyu7BW1GrlQK7NwrQbgFg2ePpNCv62Pzz+LUkXl4UH2Q\nKFRBhJ+MMmq9Tx4IIAUrcOx/wc292S8+VOoH404Scp8y13dsW4v1YmvbO7zobomF\neTA1TZ371q7OqumxtCl2B8gnAFxzBPH7W6O3YHSs2zEpwtd3r20VS/WZ2owl8+HL\n1hGg1AY+LCVBsX4BiBF/4e4qr/6BghvScximLbIThS9jLJCJs0jphhPKudS7dMAQ\nveGSlWJUJ1stLqoWkmQuo0ndArLtnHQF+pFRtNcaIqmn2ZOQM+GD91PxIlMmR/hw\ndKzVf6b5ncwvfS2di0ySl8v50SgBvZTbLm/C2Qh5VcB9NolzVHqICtoVO/fD4I3P\ngS52Asdx6Deeq188yoXUiQJQBBMBCAA6AhsDBwsJCAcDAgEGFQgCCQoLBBYCAwEC\nHgECF4AWIQTE8N//TowagjZAnQjnO8ZBzBH0yAUCW/Q5HwAKCRDnO8ZBzBH0yPtu\nEACUVtMYUeB8NTZC5e4ceP8D0fciwxvcXKngRSm/BItY/Hi4gWn01Us5cG/7hh6y\nnW16f2DLWMTQL8EIcUZ7kiKvBCJc/v6X/4XCPGJthGrsiFzPiaFUv2qzhSooQkGB\n72Qi1WhdvLn1ocSkUDWbFeEJCnGxH5bVBSntDu0nVU8DwVTB/NQ8V2HZDl4e/mWo\n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\n4G5Uhcw=\n=YIVB\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nH9y08K6zB4JFN7saJiCj2Q7W2nXuEUhRVs2K\n=L7AX\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF222c0BEAC/wIiI7EYmA7yprNa/0en2leF+CrF09BlCItTHH5IgjSLGq2tI\nBi3hIhf7TitDlu6GphHlFjvhj6UDgdEmr0itcoOLRhtER6WlmMaXtS+im5fPSLWW\nskZSAh1YC3iqOQCErkAnVFUWY5nUbWfxgv0pKrc5GTT2RkiD6ngor5YIAkRaYQ+n\nniHsekUYOuLln0p4n/K0/iRw5NMok9Q2FwlEj7H0kCfDPuqsgfEoDXoVv8QSVIpB\n1gdOQ7e2MeMAB6U5o0OjqzMxPUrIkDmgGIBvCgcCN33lMNO4DWlhr3vF90EhKvWP\nLy4kTa4Gctt9f5kICzb7AYZJG7+F8hrVHpbF0fXzTgZsO/BBf4ERTMKGfj7ZXq7e\nGARTfDgzR9yxWfxfo47jUd8amVk/qj9O94lIPobEUeP7SKPy2jIRTx04HcdMDPAF\nokzJf1cwCghc19mN+ro31rcnud0Za0EM7Yxq89GHvHiUEzxj2XWny3n583V4Lh6c\n2bAm0tcqmJlLholeFo3qW/nBSCde0BjwfamKd4KB80tsF8Qu6OdahFT8ybaCA2ls\ndcks9uAXXqwyTmXhwe22CHk/919Ubk0DFPozgj0AaDf+vQz+lKxDUuY0FaSEo3sP\nnw7JmG1fdz2jAw0UvK4xGrYE8nTlF85mOBhV7zjwW57b9x07Og0zilqZhwARAQAB\ntB1SaWNoYXJkIExhdSA8cmxhdUByZWRoYXQuY29tPokCOQQTAQIAIwUCX3X/5wIb\nAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEMQ87EXBerk8y0gQALZv85ID\neHIDsbf05A3q5G614MQzRuU3rXBlLpTEv6DJorQll3hhmvRU4aQzX2D3JTzOAb8W\nsu+cHUXJvU11rCIsAQN3L5yWEG11hld/oDG3j5S0sM+/3YvqKjluWhQpYvOrTQUR\noJqn5UvKNzZ68cWgQDJOS3XVw5SLstB3ctoa3me0sSkSVs7Gy9L2SJUnnc33GGEr\nUJCo7u/0eD5+ihNnP4buga+xd8w4zqe9gEzh8BZMi+soAv2kNC4wlMbErJUh0Msq\nLTEJKmbNsVeGX0gwjlJR0sPGbfNWlN4lw6xAljFeRjRJg59Z9gk5P5+UHabUZLFk\n9U98dj/8tclmxoa9ceMW1VVOBcpVskhUpF7DK7j0ojjz9iRXXWIxkn5dgnAP58eK\n+WZtSQ5lBbzyLFFlr5c51/FZoFurVACWymHNiyA3Xg5tsv2e7a31T1oPC0K9T3Zx\nnavSgnx0Gb0SRJM2W0kJwIjdX61iHN8yisnD4+DMHWGZmNgPN/tVhpSVKVKJC1do\nWlDgaoi2agnRtgfI0G9Em1CN7yHBCRpWaVERAaP/+uBogAwWOZFNgQjwVtU6jZYz\noLgazKbQXDf859eJ6C6AwWxrBiMMsdLsuQoFTmAztZS3efbLXU0qO4NkQ5QpEMP/\n9U6873G4QApdzEHojTyTt18ffmvZjkzpbj7WtB9SaWNoYXJkIExhdSA8cmljbGF1\nQHVrLmlibS5jb20+iQJRBDABCAA7FiEEyC+jrhy+3Gvka5NgxDzsRcF6uTwFAmDR\nt8QdHSBDaGFuZ2VkIGVtcGxveWVyIDIwMjAtMTAtMDEACgkQxDzsRcF6uTyfNA/+\nIchjvyChF8f5CEIKvfT46yg3JrCl8sETf6cTR+ZY6ZleVgHD4/AM6yZXDg6B3gfj\nUgZMpp1fu76N1zY+OQ6fPH9hgOGEsfKX//n5AJCCxhESXPlxQ7rvAkOLqUa8UsML\nI8ueh19Rv+afEL4Z7l+mJF3HjGZAInVhbXg7OBp9x2Y1YhyGQe074bT1gX7UPdfT\nPmmkT7cD92ec1uxJVSYbG4UzUZb46fZSUmgyRAlz3sTng2y7an9t/auzPNrzm+DC\nSplupDqwu07MrfiKUnGfJs6C0tx9LGY6KQQKZR5yCVqp+bWhRuFd04e8gIX2qq34\nvgU+GbulJoz63OJ3tu1igouoVnS7OwoYrEIiWZI3agzkb0VvpAlqVInOrXmZtvFn\nV3wLW7RfXf6sO3sSjgVBGCunxNasbaTCtUk1jpiMQKZpg1LwSEcLEAXt5wIDZuWl\n827/CHSEuLqJNkJsTRYXZe/hXvWm09OGSPQglcAeRH9fytWxBBMd82LRcsuYw/b6\ndxw9qkwaLEZKN2vtvdJYu3BrS4PndU5Z2T530rdpign07ZpIDPvxElJ8KuFRgikN\nFujKeAcMQ+PaZVWXBTEXsmqVJNPpLcun+Rb+ufpiwhehlqbnhdyqF40Rxivf2pk+\ngy0bKrCl+AEYiqJ+dKpUs50PYApvQCoKq6eVjoEnsACJAk4EEwEKADgWIQTIL6Ou\nHL7ca+Rrk2DEPOxFwXq5PAUCXbbZzQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX\ngAAKCRDEPOxFwXq5PGi/D/4yqrdmfkGqJBDHXfE6wnbm2+rtpr0F587CFtKeC7P9\nlJzDBXGvU22nf4pYB1c+UsLRvM+l3pQAfxJe8lg1fc6Phso1cb9WVquqHhkPDT+c\nlgkGV8c0M9T2lRpDOLXmn7UfoqVq6aGCI7fqVf6mq0YCcprsAx6cPFd6G6jF6qEq\nMaw7Yx+6gESRymI2FbifEHAEOWZHcS/H+itRN6OQG76n5W/PmVO8bsCqxTQlTqLS\nARX7bMFVI/J/F00DNjqI2sWzNx2FLsEbfG+NSrnGmun/Dz0M//TTwpAORTdQ5xGF\nHcxNCsuNeRHVvbArP7NXIydMl5mBCs8W9fAZkNVchiVVgMovn+APA0/zhCjw/+I6\nEwGnPtOYzENcriKPF/5ZTrv/moGVWktfdgTO8Ygbx2Z4953U0JMAQc88CyY1Rnw7\n5S/tjmT+hwlGMb+YKiwq9+P3Zo5+9dPHz+aL4M5gskZDBP2Iu+c0ixgukoKdYf8d\nmje7UsuF2RvW0y0Z/wIhNSo6/N/hYr6i5cMoDeZ6zMVZZEyFPx/JnvAX6B7t+7Br\nMNFRtjvG9TNt5P4BkoBy0fL5Admid/oWlDLz6posj5ayfjb4ihB91BI1ORaqG8oL\nAUC6C1vb8dmvCPGIyM0pmJnALc5/glnDNqyOVRrYAQXowNrlVPdbX3dYbZ0ooGAe\nwbQhUmljaGFyZCBMYXUgPHJpY2hhcmQubGF1QGlibS5jb20+iQJUBBMBCgA+AhsD\nBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEyC+jrhy+3Gvka5NgxDzsRcF6\nuTwFAmhcK9QCGQEACgkQxDzsRcF6uTzocw/9HMMhhddNoJeqOAFnKSKcKm5uw1cc\ngTezizurj6W7GGKmJAvzLHz9gD+xz3TjAe+E4ZAkQNF2x0OmuEiHN1ST3lqtmF1J\nfz3C1/WvtTTm03h6t5NQRtBzCLhXW7mH6Qbof8yI3pozM9t/mhmvkkr7FjJxC8cO\nA8VbNXIVChZrRBrQZXfr06PkjDg7qEzSKZgDA10oSBJexZksf8g1L0D7NuEUu3dL\ngwrL2+FlT3RCmEEhEei//V4gTbwA2DXjgro556vVv1SlgJWOsaD3228XT05jSta7\njKswMyhS/U+/e7rt3BWhm/sxdHpZXOnQ0tqB8/veu4aI79DFTP//oDI45xwXXFSw\nZwF9GEvOlX1q5ZUA4eS2mxY1hHrj7Lo9VZwW1cZou5msqKXpe6+CLcSjPbIPT51V\nVFTbmx9ps+0sxCa2cbNu/DvOlxAmZj2c9ceW4fsAiS4m8K4Z0nM8yy+t+2JHh8d4\n/GpkLMMWdYGq3xTxRS4mCekEZ28LWvwjo9/X06UvSGSu+Z47VxmR7oVeSIt8gPw0\n73gAYz4gb+cXTfAm+LOk5i4j564Ixqh5pIkjwasMQS5vlswWETq1hmDsCDCNMgLI\nbbQYlUQgCFClPl+i4TXfWc7b0K3AUSjeThG4ZpsjDhhAl/uD6ykNMugZt66XEJoD\nOvzI76MwP0rwuX25Ag0EXbbZzQEQAL9YA50WXd+iCK2rTlDk4Pv8piGkBu92CJ8Z\nlbP37AriS0xKYm78sWFqRwflFoerUwdPbC7PV5FuslRC3Y7T2T7uJOU08YEue5nC\nXgLJxnbtpjCKoLlgDWgLgLcmtfQnQrZkIZmYQ6zlTFHrJZ+Mg29ku/JeZ7HHB3EP\ndQoFd0EIJJJy5IUhJA10xnjND3RrSaApS0Q7swui8LXNOtLa/tpGXF/0OA6OjIXB\nxuT8lgTTUtk6zz+ESw0BgI+GnYZmL65m/PSfTVd79PujWhP3e/OmDti8JxWVsvjh\ntbmS2ZNuQEKAvGJS/ZoMrap2cuA7OrDM0/Ndoq9dft5yhWZbkwBNevPwXSo9ZVgt\nK+mZyVrQPKTfPusjF27HwPw1xrSvj1h1b1bAlg19FJTTAv5GqOePlnbgUlPvJYt9\nR3gipBy2ZIwWV9dfRZe848dz3dReD/+q1fNkQJJC76pv7T0CboEMHvKryKfsKNp6\nTHyHWRE472kkQZ7qF2ssxu3d+JHFvn+KEAmIbR+UfS5hnENVallHXRU4gtEAae8c\nBlXNnP8swzO6BkgGqsERtvATTdb8xjRSUIcqPxv7EIRG57lnEHuGr6N1Xa6FAxTm\nhOy27AlmnbQyaJmpDMe5s5XHApUbRA4Mq1qSbcOFNml3zUVKnCXaXY0B6MM5F3RP\nhRnx9LHRABEBAAGJAjYEGAEKACAWIQTIL6OuHL7ca+Rrk2DEPOxFwXq5PAUCXbbZ\nzQIbDAAKCRDEPOxFwXq5PLlHD/9kI4bENYzm/IE1EK0zp48LZCsaeZGZ/hyW0qJ9\nlnLyXKRmilHAeLmD+tRXEnCOzmFwqftJpQJditx79uoyUiTiOA/yexia4/hrItZ8\n+E7lXQmvA6vEFRMNZ+5+TtlOMMS8BL1kdyJIC589nDZorA8l8401e9nAGVtowhk/\njWpF3tuRfb7sVKvvWpzee/EJ1ntmUXi8FrhflMhBJafQaRdLFnrGTjr3iwh05TDE\nAbpwzTfZQkj6YljJAFz+QwNZV9mK9hGzq1ExGo3B7EXdY7qelQIYTEByPXkRXa9j\ntUYJZOKX12hyCYfONIFFcZcNryxbF+X7UtCnTrgyK8LZ3L1t2xKOxumvoeLnfOqX\nWdizm25eu1vaBKSmdYwwC9z9DYZqImYtOksLQoolqEh0LcqJBKJuItMKmDsBW5EB\n5IFinbpmzxa2X4rvX7qNNQ46ky0sJzLW+bollsrn40S021IUyXq/FsisJTyFz7Nd\neTvEgr8uvOZJf++r9oEZw48siZNqjvPsejQjacu9UyXM0dCnJSaOm8OnZy8TpRB/\nibYKg149T1Y2+JrwHaXdKiM0U4k3a9X+TjA8BCV0+m3eY97cuFqXItaEGg/3FJyf\n9l5FS1bmjbondfTk3+rBsZ/tzDds+Jw2ir2mncO1jXgVls8xieBGoRWvH1N7nSSM\nNBG2Fw==\n=Cg9A\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'DD8F2338BAE7501E3DD5AC78C273792F7D83545D', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n8FLoLzom0A1FNxCvgzOraMITOobs\n=dTMc\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'A48C2BEE680E841632CD4E44F07496B3EB3C1762', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nfyCIa9sx6lcYB+gDJgESL0Nt\n=hSfG\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'B9E2F5981AA6E0CD28160D9FF13993A75599653C', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n1j7Q6fERAXlFlv7ciEuKBz5A3SK7Xt7Eb7vQ\n=kARQ\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '108F52B48DB57BB0CC439B2997B01419BD92F80A', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nAI4=\n=Sf/w\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '9554F04D7259F04124DE6B476D5A82AC7E37093B', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBFTROWUBCACslqx8p2znj3CwEqWK+bEgyfxykVC1iFEABB6UCQ5UrlAiYTjI\n0vwFaTlbWkD3dWBxiFN2+n24Lro549ATmXGO+i0Sacr7DTgawZdkJuM17nNmlAW1\nc1lo6D/KfQ1K7QrakloQw10LzLLW3uxi3OXVoefY1JKTeQn43jolrCHho9iNvZ0N\nxyihQYzSiRzqMQGltKw5UDdbhwuzr9jROgHuUH+msOzax9IkLb8mm2VqJ3HaDJWk\noAaU92leGoicEBhR8qC3zGzua7pRFK8qhjaSSL/N/NpCu4ud6axHaBomfn5HJQNT\ndTHB6R6wsVgMED9vfZHqJeyltDvA8FDw4uQnABEBAAG0OUNocmlzdG9waGVyIERp\nY2tpbnNvbiA8Y2hyaXN0b3BoZXIucy5kaWNraW5zb25AZ21haWwuY29tPokBOAQT\nAQIAIgUCVNE5ZQIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQbVqCrH43\nCTvBaAf9HEU1MhfIF8qlGno0WQojtEW8hLskw0Dd4OeG3cLsSmCa72zEjEVHw82X\ng862h3vfEybf7nd+mxdoSDT3iLwD8NOW6pnzW4/RjRGwvKKUhYggu8oi3xBSNj+h\nGRg2dkRWpCtqJo1q0XUdh34iqJUpk2isYvsQuHJrbcglPLO+tuFFNZ0JTf1f895s\nOnP1f+N1ygOnhehRoxO8gjncU3M8TXSsX8LQuCYfrBXayT5kZ49j5/+HOS8dHxYE\nR9e4LpPIWpfWQtXgXZKxcpyu0x25ZO6jkQLhleXtRpCfDO3DJw7l80suGf7Vjt0P\nXiYk5K2Z6x7h9rhWkEN92kTmRSbErrkBDQRU0TllAQgA1g9MDLyGI3/W+oXwrl2r\n2DKvJFRALeUHyedU+oaGo47XBKvNk4UdIDeHuUvf5DzYJW6t8GH1G55ZdYGik9/M\n16/6b1eYA2T7P6kjOmCGtv3KMkCLbymlFxb5SWEWwVR16L+5eXfVKbEBT7n2MGwT\nc42DCJzg7APZasaJuRQDoPgQFVQ1WX+0eXimAgdYlbtVjZmrkaIZbzGIcET1lJdB\nxOj+bR0pXIXbOvo5FlCVMmGAefrDePhqfuFMAQup8d4Gd7V19AcV8FyNUrVkji/a\niupi4FcBKLuF5yybJLY5d/Ij0pZ7MGUcbUhe9CbOZv9fvZBnL1Ukl9bxfchzrM2h\nVwARAQABiQEfBBgBAgAJBQJU0TllAhsMAAoJEG1agqx+Nwk7vzAH/iTv3qDimLGA\n9YdgSJ0h1aWZBgU2okJLLEeZQQuNHzM+ekQDhBA6Bdb938VtNUAdCSABBKUlFeq7\n+cqpcH1YgHyBv6K6avTI0F2Y1+nYIqZoe4n1QlcWg858fg4zzjBPU+s2eVIl9rOZ\nkMioU0UgM53DOhz/KjCHIWeLCpZAdfXsK2+lzWLINoYJ767wTNkXWPcxj2mGtht3\nhCOcTHK5toVhlytM7vGVacn0xwOhNSNMBwFq6rucIi/fgLheNDFgLPykE48XmLau\nfb3Fyii+q1BCWWZsyZjkLW7kcpf62i/YlrJHjWRheWLblLtxqy4ZiKqqLCtyfh9a\nxr5u2jsokh8=\n=2Ugv\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '93C7E9E91B49E432C2F75674B0A78B0A6C481CF6', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBEx8nbMBCACjmblSAGggunFHAWRGZLWLKltA2PG6rIM0bOokJFWtGwRqBCAa\ndKuNwBGy7eBwFQAxuHnNwDKqyGgCKUBe4RUuz0gKr1WgkPAzHZppo/n5BhE4iIxr\nlWJBKVKk/gjXVUBPhRPWNTa23JbzJHntxzHqeMQWIwIdBXoyOf0tOuXcfH2oVBdq\np8nFaeJQ906F6olUD8U12LgIhkg6/NTDLymwkau4YcOe8c2repNITVFaP6w5FmSj\naWTnyFXpdnVF4fWpbrLXsPBG72UMsAyWW4XuMtO07t4MvzgKBQALmGqlGMVtVkCI\njBgYD250NqBGKPmBrfJQVYbEesOZqNAF7bdNABEBAAG0J2lzYWFjcyAoaHR0cDov\nL2Jsb2cuaXpzLm1lLykgPGlAaXpzLm1lPokBOAQTAQIAIgUCTHydswIbAwYLCQgH\nAwIGFQgCCQoLBBYCAwECHgECF4AACgkQsKeLCmxIHPYyyAf8CUkbVGxWid46g9jq\nhYJd4o2Au0l6Y5prhraugbMw72PUstn6VAqx77Voe4NS5J/8942HHWjtlMIn42gU\nB5DS6gnGDFpo/64d3AZZ66meOE7OHcD2qo/kvziIPq77paOu4ZLF6XcY1xycCCkq\nL+8zzH409o6uR/LN4APeUcCmnqugibA1vOmQcvU1Ks+X0hKEHOkjDjws8F3yUokG\nahUrtvYqJ7qNx5RQvKzvcNbhM7omZ2KXH57YyhyvSdUvfjzaFv7D2CABDlVk4xxT\nGAZjmAKrKI6mvJLU36Ip9XJTH4J7iAqOe2phUSmOdZOfRrF6qFkvmNNNkH4MbDu/\np9IPOLkBDQRMfJ2zAQgArSXiNKQXWZizpdNy/C5fWIqmczpW8MwnVY99PfB6+gnA\nRxZ1GX4J1APd1rniOEOEEgWiB+QsihuMLHYuGHBhQo1zvrrmvUAZpNwz4NovcCAq\n3AzMMk+VQYDYWqqkUFnct1dLblf6SuuZ/beWUk+Rju1mRQoi9heMMwjvNlqPBc0v\nN8y3I8WhxCtQpWIMvQs7LN3v0s8AWNCV9VsQxmgtYpx5kMe3aYMJWEa+RzqhNvgP\nFwfOUaBXQ07dBqLOhTjMomEVeBoCkBU8mrIrgqoOkucKZE5/Hlqb8d1y0AiO02bs\nzCHPc5Wx5aCpq+EYWhskM8Zg3CpXhHQx3rE8oMnX7wARAQABiQEfBBgBAgAJBQJM\nfJ2zAhsMAAoJELCniwpsSBz2bckH/0+0te0aioSQWEb39JYZCcBvXvvnpL/pHgPa\nZ6tOizCf8/3tSRbwxI2CQ19p8ji95v+A0c1G4LwiTfZY3lEkIk0ZL1M1Ky82x/lc\n6hhsIVQ3JdrCSuOUzMhgQQk+I+xPr8gx4+1GL+Ia9F44o0CP0WgPeLshmK03hmjy\nZqC+u0j/R8OQqS/kVe7qt41nmNZzwsROuKuJt1xFD2GMquL+Z4oCu4YLKM13WPkd\n+ntZI9Gu0N7YDY4tKQCtcjs7z7AwVpfCesnuJtMtFb1+xuyt5DErMVN83Mi1VrUu\npYtVXNkSmiSUWMmh7QfVNFrWDqlP8vUi3BIEV3FtjXblPkbKbow=\n=o1Jn\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '56730D5401028683275BD23C23EFEFE93C4CFFFE', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nOweTnxa0BqWTj1O87/SvSKdOd9ZI5GIp\n=K6ke\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '114F43EE0176B71C7BC219DD50A3051F888C628D', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFSvCTEBEADIa8J6pku+vT9RZ/cU4wKmC441OVghEZ8Cuct4AynkZQZ8Hpra\n4mq9SEjB9t2KSM7kvN/yBrrJBJwAnR7Q+e3+gfthL8Hmgr7K2J+qJdSm7Va/LcSK\nzZjN8UX0EFC+gh+gda9qm0nwRTgxR9k9nWu3l6zcvEsLE79m9Uir3W8BtGak9HCU\n69aSaaWupoqTXgPvYuh0LaF5C+qEccUls/Bmuw8FPP+T8OmOc5SU8pD9uz2r2S+p\nrBqh6rutBBqzPRl19nR9MCv/2jYmPqyCtvBbalxy8fvxC4wuBSarkgUD2dj/aIqa\njUMMBDFELXxFwTKd73RQtxnHha9VsbIFpy5wpt0NBq9/Yi2ZI44Q4PqccOs+ByKJ\n8iZsvg46WDwmZ58pU8Bxkl/Qaz9numDfGYYV1Vhk45ACob67zYGDgVE1X2BBe5iY\nJIA8hergbPZf3j5DA/5cCEONqTCLc70XYWE31x2+6DcywZ2xxJfMw8eEOihCBMwe\nrNiMYH0uENEo/b6q71g23rNC/mNhZCYF8k8crbCmbuKDNjaQA6zxQW9E4hzqkkte\nTfNRi06il3fAKbkfDjIX0kH+/XGaCd0rYK3mvx32tVzuZmk86EEFot/+OOAh4zkq\nQ/DirFRXnGCbJVPtyGWO65CutAlFSoOaVN7G3tJhQCQGg5jb41l27d904QARAQAB\ntCFKdWxpZW4gR2lsbGkgPGpnaWxsaUBmYXN0bWFpbC5mbT6JAj0EEwEKACcFAlSv\nCTECGwMFCQeGH4AFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQUKMFH4iMYo02\nxQ//bdrpGrxbSlZnC+cMS0dRzZ6zbW/r1QPV/Bag4GeWl61BhMSdGg99Feb2kkqH\n47P7wv23PyzGT1aaUnMwlQeR/tVz3gVCHfe1rAQ6pNtZYEA5b/IF4RhIYoZr+9ke\nwlNIJk/zSCVd6110nzMrb9Z82cJiIb9MPiYzw3BW3Gzw7EsdGPbFwiCsRZf4BApa\nHMY+T2j8AlENDNSOtsvlLwKRiSQqd00XvEciZ/xtB1VYgNVGHN+21sVZdadj8+Oi\nH2iHnbdcElVadsCpKdcmETqteRyrXXA0FW5WPEM5N1qNWT3Aml4C22cU+S8Kdx79\n5uPKUQjgOWl6/x6z2HxSPWMOGVS5+X/r047vOGa3kap9RbOTiyxQNixcsxomHUQL\nJcYU+K+dzSeuXY6uc33gSY1a9QkKdzWI8+ECbS7CaKg/540AOtRswbXWxpRpGz2l\nB5sfN4g53aCn1XvcMDWtjrkXNBn4tRyoH/YUohWU4IPBtyetKSr5UgngHNVBkDNF\nWKn7vpcfeM6Tc40cdyQEvc+bmVdcsUmBY7wzkzJTyIf9YBSJXD4MhN0PzJHdmNvA\n9R3dyv4beYDKWqxxgH6/f6xVgI7nPM8UhDbM0FcztwbUcrM+jX1uv/6XR4/AYE2e\nUVRfplMrlJKoZlX/5wP1H0BX7xHdL6P1S4AZg2K9INKeuce5Ag0EVK8JMQEQAKdI\nri92rNrpBoU2vhVsbNNGaJV1A+jDvyoOJ+ahbG2EJKMjkOHZTfW5ZrO3GWpmXQJ+\neTCy3nbBkKTNI/HxKc7xU1WpnK/YRyRtDOJrUN2MiMvo5HYmv+g5fDoG/8oS4KXZ\n6nn09UCZPwo+B00nKWLFIcFblMvZh/a7h3r3kVu4yCEKSu4gO7Vhdz0wHJhy3PSh\ntRDh9BTjHaW7R10o8agwHMXKeoWyOZUZb9QqifbnK98gVHmw6qT6FlY3czFWtSUk\nbk4O3Ew2tiupguTrenC8+Mp+qyd9r7WHJZrKfNYQYLD32eojXvd3VCrSmfYbVUlh\nmvSFTE/95d0EMAuxAodKVtpayqyQKDXKfb2X2gtoCWKaYtxYlh/XVqiuh+BYAZzA\ntiRLmLe0joPhm9ew7z5PvMJ8Xl4HU+BX1TexNfviFvE4Q7xqaEnJLG82ms/pPzAr\nx5qIcUPkEtR3vu6kyvk3AZXc7D7ZQtPhaN49CNES3g50rvF7bY2Rw1xBfxMUpUyA\nq2btT31NWIj5Hrx8wr/ivPxMzxpNQu4ujuo74f4ilaXlsksNxN5JPPDCmVc8pRMV\nlIhjd7T5E/4dwiKCt14G3Lgeoynln9czO80jasYTxs2Ietl5EhZ110DqUhG9GIId\nf5d3Qb56l3Nv9Shrtlhu0q02aQayaP2HkFHchXxbABEBAAGJAiUEGAEKAA8FAlSv\nCTECGwwFCQeGH4AACgkQUKMFH4iMYo1xPA//TKoprQhJ365yHH/h2ZqSXP0V4UkF\n5WGxzf0k+BBKzgKnhH4yCBwyRU1txbZ+V4mTf5odoKb7h90NJNzWgLDqkURXAK/M\nCRc6KdaGiOrIXNWQKayCDwhQ3LXU7FstzTzPFKiwlYsRuCw+36CSEkRzB+onLoBA\nVOzh1QAlLLO+hdUDs/OswS3GNRg+HGltazgy/LZ3nJ2lsqXOmHGgXe+P8eZKTvJV\nrg48rmws5NbhK4JHA7oVvJb8mCaOvsOh3Sj5BTgdJQCTClYqjyhcX3D54OQumXYL\n3pAWHZ2+po75Ybg8KzcDosgYl2+2byxVDTWv4muP67t1Latqg0LmJrlsjxdIG8s4\n1WMdXvei9kXU1qr15yIsGVQ81tOxmb/m15S3JyubyQXVP6zfqUrhS8LGk4wpEUyw\noUyfo0DMkpIwSPlV0KkSo5pApY//cUNr9MEYJDUTZGgclY4JxtF18LtnKUmb74Mt\nLD0YJhGRrdrZluYcl9yBfvPDEtb4PmD00dlpyUk6ehf38L3XOmWyCxqa6jjL9dax\nTNYE7DPKQo5n2WKKuCqjMfyeRnwCqgO4BwsocxPhZl3NzW/mH0orA2mxwg/6MJUF\nk7rTC4J6kM9hoUCMOJ0XqBfQZVeQd2x3bkZpx1ubH6L5ilZHKSvHcqOy6HE4wyDz\nUSXWhEefc3s6beE=\n=v6Nn\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '7937DFD2AB06298B2293C3187D33FF9D0246406D', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQGiBEPOjg4RBAC2iPU+EHukOrmApMnhYym03gV/VbdPDydVVj+fc7TyjULlKWP7\niEMFZb58EBjjK4Db3O5JiC9yZ7NRgYvqCFGP7hTVBTykJucXIyT8wCmaAImrtAlg\nhA7LKTKlMdkPMz+iTiaVivt5F2W24Lfy/f664J5POcUH3Y5e1dhImNY6fwCg9TN8\nT2QKrFuudsbvqdw+w9I9ZD8EALBfpb39xdZenQllhq6MnODIzeHf5+wQV0slAzaO\nRZCAVWodGtNx3NYomvmOuhY3iwFrTiGW7r+3CDqt/4us1vhn/y/A8JijReltOeF0\nnbTM9HzD+3MrlTZMSfR9lzpu941IDkvw67NeFdSHPxLEEcAxI+eoS5dYsdjM3isb\nOvv1A/9jJFYcPg+CRDPYVsY7ok+JW6olXfDzK0pg9zKS2gNbYq2AhyvKKjM/L1Ca\nRkmpliBQZEq3QarXrirdwtR9283JKQ7K5UgigkCDqxE2C7jF4XZ6BA41Wh63GPFF\nhV1J7R+LI/FZiG0gE0y+bckeSB68+wdh8jnb2JCkNkegbBU70LQvVGltb3RoeSBK\nIEZvbnRhaW5lIChPRlRDKSA8dGpmb250YWluZUBvZnRjLm5ldD6IaAQTEQIAKAIb\nAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAlMeUUgFCREw9rcACgkQfTP/nQJG\nQG1lcgCfdK0g8n4Rwe7ng5CiGsK2aeFu3tMAn2GJN5GDVyVpxIvDMAITPIcbpJFi\niGgEExECACgCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheABQJVFZjCBQkTKD4x\nAAoJEH0z/50CRkBt9DUAn3JLT9PifudI9mbufjOvNhTRjNzBAKDnPR1ceqWO3F78\nO02juhMZp82YfYhoBBMRAgAoBQJRPNokAhsDBQkPT38mBgsJCAcDAgYVCAIJCgsE\nFgIDAQIeAQIXgAAKCRB9M/+dAkZAbSbgAJ4q2SMC4N9bzJwiU1fdz8evS2z5xgCg\nhyS/CRrGGzlBbaQfLsY2h6CVftO0MlRpbW90aHkgSiBGb250YWluZSAoV29yaykg\nPHRqLmZvbnRhaW5lQGpveWVudC5jb20+iGgEExECACgCGwMGCwkIBwMCBhUIAgkK\nCwQWAgMBAh4BAheABQJTHlFIBQkRMPa3AAoJEH0z/50CRkBtgOgAoKCVOmY7DUec\nppH25/ZNtzhUZUBQAKCv6nNAXhafNJ1KEi9nPLsweUv6CohoBBMRAgAoAhsDBgsJ\nCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAUCVRWYwgUJEyg+MQAKCRB9M/+dAkZAbSXB\nAJ9T4QxHcQiglAj6unDkpNCfQd8gAwCfd3zAVN6xtlDI119a/Gvb5H2JskeIaAQT\nEQIAKAUCUTzaRgIbAwUJD09/JgYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ\nfTP/nQJGQG0vcQCfXBAfor5DHZEd0vrUAtHoZXIiRNEAoKs9sryP9dD65Uf8P0eL\nW7NpB/Y5tDRUaW1vdGh5IEogRm9udGFpbmUgKFBlcnNvbmFsKSA8dGpmb250YWlu\nZUBnbWFpbC5jb20+iGsEExECACsCGwMFCQ9PfyYGCwkIBwMCBhUIAgkKCwQWAgMB\nAh4BAheABQJRPNqyAhkBAAoJEH0z/50CRkBtqIwAn2HZ88QHvZFReoqNo/saO4ax\nzqxyAJ94cWr+bwMO8wbx2JHTtGxRKx+s+ohrBBMRAgArAhsDBgsJCAcDAgYVCAIJ\nCgsEFgIDAQIeAQIXgAIZAQUCUx5RRQUJETD2twAKCRB9M/+dAkZAbe1gAKC/YnU4\nK3YvGN3zDZZqA45F6dGUywCcCzQs5rgW4ivr4kVTWb0JJXh6BNCIawQTEQIAKwIb\nAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4ACGQEFAlUVmL8FCRMoPjEACgkQfTP/\nnQJGQG1yCgCgrxLj45QV/4cZ8rarKJjetNdBj8gAoMxW6LSPod4qwTE2jw2Qf4wP\nlXC7tEBUaW1vdGh5IEogRm9udGFpbmUgKFBlcnNvbmFsIEtleSkgPHRqZm9udGFp\nbmVAYXR4Y29uc3VsdGluZy5jb20+iGQEExECACQCGwMGCwkIBwMCAxUCAwMWAgEC\nHgECF4AFAk1yezwFCQ1mVBUACgkQfTP/nQJGQG2ITACg3Zd9owEpcatNeuPrWpmL\nM3yORKIAn2kmpLHReRpKQ+buF/I2SUq5uLXbiGQEExECACQCGwMGCwkIBwMCAxUC\nAwMWAgECHgECF4AFAlE82bQFCQ9PfyYACgkQfTP/nQJGQG0qEQCfbXfhJFcYc/7k\nlu56ErfoVejcFdEAoOdUqZR1xrvo2KlJ5VjLLfYQSrjAiGQEExECACQCGwMGCwkI\n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\nUhw2iju+XrCVJ/AXUtwH\n=AG5V\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '74F12602B6F1C4E913FAA37AD3A89613643B6201', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nni5lUGYmUmXIuRub\n=EVa1\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '141F07595B7B3FFE74309A937405533BE57C7D57', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=48+4\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'DD792F5973C6DE52C432CBDAC77ABFA00DDBF2B7', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEZAFttBYJKwYBBAHaRw8BAQdA9UUQNclFp0rIrgtQnNw6BgjDINkFPoVbuS4H\nsQNEf+e0LEp1YW4gSm9zw6kgQXJib2xlZGEgPHNveWp1YW5hcmJvbEBnbWFpbC5j\nb20+iJMEExYKADsWIQTdeS9Zc8beUsQyy9rHer+gDdvytwUCZAFttAIbAwULCQgH\nAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRDHer+gDdvyt3PYAP9A8XbvZYT+N7k9\n2xnRMfrAUep6TLfWbx9uf7k/hm/+KAEAmqUYV1afcuwU2xXE5I8m25VMnCEKFFEF\n52tW2baWkQW4OARkAW20EgorBgEEAZdVAQUBAQdAtwscgbxby3vew2hn9F+KlVPL\nvFBPjnvODcnsqlO2mXQDAQgHiHgEGBYKACAWIQTdeS9Zc8beUsQyy9rHer+gDdvy\ntwUCZAFttAIbDAAKCRDHer+gDdvytzxaAQDvYX4o1Y6R30bYwIXemGgbO8GlCkgk\n5it7MjeSk8vUygEApE5zIi5OtP8TlPiMgu2MoJbIltqqDCKWTtHPIQRNIwk=\n=IisY\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'A363A499291CBBC940DD62E41F10027AF002F8B0', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: A363 A499 291C BBC9 40DD 62E4 1F10 027A F002 F8B0\nComment: ulises Gascon \n\n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\niYmSNsSq9FUzTw==\n=CCuY\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'CC68F5A3106FF448322E48ED27F5E38D5B0A215F', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nZWf0a3VP6Gco5bmDPhvGoLEs9Vw5\n=57kS\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: 'C0D6248439F1D5604AAFFB4021D900FFDB233756', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\nTh80Z8v5rFX5xNw+zn0Ee+yr\n=SZFY\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, + { + fingerprint: '5BE8A3F6C8A5C01D106C0AD820B1A390B168D356', + armoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEaGA63BYJKwYBBAHaRw8BAQdAo/yU+MutacFmmn0CEX495goNrBxR24235XLM\ncvHYjfq0L0FudG9pbmUgZHUgSGFtZWwgPGR1aGFtZWxhbnRvaW5lMTk5NUBnbWFp\nbC5jb20+iI4EExYKADYWIQRb6KP2yKXAHRBsCtggsaOQsWjTVgUCaGA63AIbAwQL\nCQgHBBUKCQgFFgIDAQACHgUCF4AACgkQILGjkLFo01afgwEA/sLHqsj7ml2vyDoT\nKDPE8n9a80ZOh14OfnlOe0cCZA8BAMEOOk7QFI69DIlV1nMiqcFCqQFoSzBU2LkI\nR17p/j4NtDNBbnRvaW5lIGR1IEhhbWVsIDxhbnRvaW5lLmR1aGFtZWxAcGxhdGZv\ncm1hdGljLmRldj6IjgQTFgoANhYhBFvoo/bIpcAdEGwK2CCxo5CxaNNWBQJpsCMx\nAhsDBAsJCAcEFQoJCAUWAgMBAAIeAQIXgAAKCRAgsaOQsWjTVr/sAPwIBsG8g6ND\nzoNRTX1wPKBvfZg1NP7tYCyM5sxQfrpuLAEA05AhG4xBILfhL/f0pqR5jXfxg6gz\nT6WfeVeS6zeHZwe4OARoYDrcEgorBgEEAZdVAQUBAQdAQVmtih8AO3ryBQMR/22x\nWHVKLjAbCiH2cMxNH+iy1RQDAQgHiHgEGBYKACAWIQRb6KP2yKXAHRBsCtggsaOQ\nsWjTVgUCaGA63AIbDAAKCRAgsaOQsWjTVu8oAP9Bc+QY+9FikX3YvMgWAqiDlVOy\no0y6UIZGBMSQlF80wAD/d34LqtVIVe9oe5NO3xA75+6Ew8tGeAjUq/ovagr5dAU=\n=JsVv\n-----END PGP PUBLIC KEY BLOCK-----\n", + }, +] as const satisfies ReadonlyArray<{ fingerprint: string, armoredKey: string }> diff --git a/crypto/shasums-file/src/verifyNodeShasums.ts b/crypto/shasums-file/src/verifyNodeShasums.ts new file mode 100644 index 0000000000..40c800d333 --- /dev/null +++ b/crypto/shasums-file/src/verifyNodeShasums.ts @@ -0,0 +1,120 @@ +import { PnpmError } from '@pnpm/error' +import type { FetchFromRegistry } from '@pnpm/fetching-types' +import * as openpgp from 'openpgp' + +import { NODE_RELEASE_KEYS } from './nodeReleaseKeys.js' + +export interface ArmoredKey { armoredKey: string } + +let bundledKeyPacketsPromise: Promise | undefined + +async function loadSigningKeyPackets (trustedKeys: readonly ArmoredKey[]): Promise { + if (trustedKeys === NODE_RELEASE_KEYS) { + bundledKeyPacketsPromise ??= readSigningKeyPackets(NODE_RELEASE_KEYS) + return bundledKeyPacketsPromise + } + return readSigningKeyPackets(trustedKeys) +} + +async function readSigningKeyPackets (trustedKeys: readonly ArmoredKey[]): Promise { + const keys = await Promise.all(trustedKeys.map(({ armoredKey }) => openpgp.readKey({ armoredKey }))) + // A signature may be made by the primary key or any subkey; collect both. + return keys.flatMap((key) => [key.keyPacket, ...key.subkeys.map((subkey) => subkey.keyPacket)]) +} + +/** + * Fetches a Node.js release's `SHASUMS256.txt` and verifies its detached + * OpenPGP signature (`SHASUMS256.txt.sig`) against the Node.js release team's + * embedded public keys before returning its content. + * + * The download mirror is repository-configurable (`node-mirror:`), so + * the SHASUMS file — and the integrity hashes it carries — cannot be trusted on + * their own. Verifying the signature against keys embedded in pnpm anchors the + * download to the real Node.js release team: a mirror serving a tampered binary + * with a matching SHASUMS cannot also produce a valid signature. + * + * The signature is verified at the packet level (the cryptographic check), + * deliberately bypassing OpenPGP key-validity-window checks: the trusted keys + * are pinned (mirrored from `nodejs/release-keys`), and the Node.js release keys + * are re-certified over time, which would otherwise make signatures on older + * releases fail to validate against the current key material. + * + * Throws when the signature is missing or does not verify against a trusted key. + */ +export async function fetchVerifiedNodeShasums ( + fetch: FetchFromRegistry, + shasumsUrl: string, + trustedKeys: readonly ArmoredKey[] = NODE_RELEASE_KEYS +): Promise { + const [shasumsBytes, signatureBytes] = await Promise.all([ + fetchBytes(fetch, shasumsUrl, 'SHASUMS256.txt'), + fetchBytes(fetch, `${shasumsUrl}.sig`, 'SHASUMS256.txt.sig'), + ]) + + if (!(await isSignedByTrustedKey(shasumsBytes, signatureBytes, trustedKeys))) { + throw new PnpmError( + 'NODE_SHASUMS_SIGNATURE_INVALID', + `The OpenPGP signature of ${shasumsUrl} does not match any trusted Node.js release key. ` + + 'The downloaded Node.js runtime cannot be verified as a genuine release.' + ) + } + + return Buffer.from(shasumsBytes).toString('utf8') +} + +async function isSignedByTrustedKey ( + content: Uint8Array, + signatureBytes: Uint8Array, + trustedKeys: readonly ArmoredKey[] +): Promise { + let signature: openpgp.Signature + let keyPackets: openpgp.AnyKeyPacket[] + try { + ;[signature, keyPackets] = await Promise.all([ + openpgp.readSignature({ binarySignature: signatureBytes }), + loadSigningKeyPackets(trustedKeys), + ]) + } catch (err: unknown) { + throw new PnpmError('NODE_SHASUMS_SIGNATURE_INVALID', `Could not read the Node.js SHASUMS signature: ${String(err)}`) + } + const message = await openpgp.createMessage({ binary: content }) + const literalDataPacket = message.packets[0] + + const perSignature = await Promise.all( + signature.packets.map((signaturePacket) => + signaturePacketVerifies(signaturePacket, keyPackets, literalDataPacket)) + ) + return perSignature.some(Boolean) +} + +async function signaturePacketVerifies ( + signaturePacket: openpgp.SignaturePacket, + keyPackets: openpgp.AnyKeyPacket[], + literalDataPacket: object +): Promise { + const issuerKeyID = signaturePacket.issuerKeyID + if (issuerKeyID == null) return false + const keyPacket = keyPackets.find((packet) => packet.getKeyID().equals(issuerKeyID)) + if (keyPacket == null) return false + try { + // Resolves on a valid signature, rejects otherwise. This is the raw + // cryptographic check against a pinned key — no web-of-trust / key-expiry + // evaluation, which is what `openpgp.verify` would (incorrectly here) apply. + await signaturePacket.verify(keyPacket, signaturePacket.signatureType!, literalDataPacket, signaturePacket.created ?? undefined, true) + return true + } catch { + // Not valid under this key/packet. + return false + } +} + +async function fetchBytes (fetch: FetchFromRegistry, url: string, what: string): Promise { + const res = await fetch(url) + if (!res.ok) { + throw new PnpmError( + 'NODE_SHASUMS_FETCH_FAIL', + `Failed to fetch ${what} (${url}) to verify the Node.js download (status: ${res.status})` + ) + } + return new Uint8Array(await res.arrayBuffer()) +} diff --git a/crypto/shasums-file/test/verifyNodeShasums.ts b/crypto/shasums-file/test/verifyNodeShasums.ts new file mode 100644 index 0000000000..5c25538f53 --- /dev/null +++ b/crypto/shasums-file/test/verifyNodeShasums.ts @@ -0,0 +1,66 @@ +import { fetchVerifiedNodeShasums } from '@pnpm/crypto.shasums-file' +import * as openpgp from 'openpgp' + +const SHASUMS_URL = 'https://nodejs.example.test/download/release/v22.11.0/SHASUMS256.txt' +const SHASUMS = 'deadbeef'.repeat(8) + ' node-v22.11.0-darwin-arm64.tar.gz\n' + +async function makeKey () { + const { privateKey, publicKey } = await openpgp.generateKey({ + userIDs: [{ name: 'Test Node Releaser', email: 'test@nodejs.example' }], + format: 'armored', + }) + return { privateKey: await openpgp.readPrivateKey({ armoredKey: privateKey }), armoredKey: publicKey } +} + +async function detachedSig (privateKey: openpgp.PrivateKey, content: string): Promise { + const message = await openpgp.createMessage({ binary: new TextEncoder().encode(content) }) + return openpgp.sign({ message, signingKeys: privateKey, detached: true, format: 'binary' }) as Promise +} + +function mockFetch (responses: Record) { + return (async (url: string) => { + const res = responses[url] + if (!res) return { ok: false, status: 404 } + return { ok: res.ok, status: res.ok ? 200 : 404, arrayBuffer: async () => res.body!.buffer } + }) as never +} + +test('returns the SHASUMS content when the detached signature verifies against a trusted key', async () => { + const key = await makeKey() + const fetch = mockFetch({ + [SHASUMS_URL]: { ok: true, body: new TextEncoder().encode(SHASUMS) }, + [`${SHASUMS_URL}.sig`]: { ok: true, body: await detachedSig(key.privateKey, SHASUMS) }, + }) + + await expect(fetchVerifiedNodeShasums(fetch, SHASUMS_URL, [key])).resolves.toBe(SHASUMS) +}) + +test('throws when the signature was made by an untrusted key', async () => { + const signer = await makeKey() + const trusted = await makeKey() + const fetch = mockFetch({ + [SHASUMS_URL]: { ok: true, body: new TextEncoder().encode(SHASUMS) }, + [`${SHASUMS_URL}.sig`]: { ok: true, body: await detachedSig(signer.privateKey, SHASUMS) }, + }) + + await expect(fetchVerifiedNodeShasums(fetch, SHASUMS_URL, [trusted])).rejects.toThrow(/signature/i) +}) + +test('throws when the SHASUMS content was tampered with after signing', async () => { + const key = await makeKey() + const fetch = mockFetch({ + [SHASUMS_URL]: { ok: true, body: new TextEncoder().encode(SHASUMS.replace('deadbeef', 'tampered0')) }, + [`${SHASUMS_URL}.sig`]: { ok: true, body: await detachedSig(key.privateKey, SHASUMS) }, + }) + + await expect(fetchVerifiedNodeShasums(fetch, SHASUMS_URL, [key])).rejects.toThrow(/signature/i) +}) + +test('throws when the signature file is missing', async () => { + const key = await makeKey() + const fetch = mockFetch({ + [SHASUMS_URL]: { ok: true, body: new TextEncoder().encode(SHASUMS) }, + }) + + await expect(fetchVerifiedNodeShasums(fetch, SHASUMS_URL, [key])).rejects.toThrow(/SHASUMS256.txt.sig/) +}) diff --git a/cspell.json b/cspell.json index c3652ec06e..c0487d7fa3 100644 --- a/cspell.json +++ b/cspell.json @@ -76,6 +76,7 @@ "etamponi", "exdev", "execa", + "exfiltrating", "exploitability", "fellback", "fetchings", @@ -85,6 +86,7 @@ "fnumber", "foobarqar", "foofoo", + "footgun", "forgejo", "fsevents", "gabor", @@ -124,6 +126,8 @@ "kebabcase", "kevva", "keyfile", + "keyid", + "keytype", "killcb", "kochan", "koorchik", @@ -158,6 +162,7 @@ "mypackage", "ndjson", "nerfed", + "nistp", "nodetouch", "noent", "nonexec", @@ -173,6 +178,7 @@ "ofjergrg", "onclickoutside", "oomol", + "openpgp", "ossl", "outfile", "overrider", @@ -278,6 +284,8 @@ "subdeps", "subdir", "subdirs", + "subkey", + "subkeys", "subpkg", "supercede", "syml", @@ -319,5 +327,9 @@ "zkochan", "zoli", "zoltan" + ], + "ignorePaths": [ + "**/nodeReleaseKeys.ts", + "**/nodeReleaseKeys.d.ts" ] } diff --git a/env/node.fetcher/src/index.ts b/env/node.fetcher/src/index.ts index 3be9e48d92..d1dcaf0d37 100644 --- a/env/node.fetcher/src/index.ts +++ b/env/node.fetcher/src/index.ts @@ -1,6 +1,6 @@ import path from 'path' import { PnpmError } from '@pnpm/error' -import { fetchShasumsFileRaw, pickFileChecksumFromShasumsFile } from '@pnpm/crypto.shasums-file' +import { type ArmoredKey, fetchShasumsFileRaw, fetchVerifiedNodeShasums, pickFileChecksumFromShasumsFile } from '@pnpm/crypto.shasums-file' import { type FetchFromRegistry, type RetryTimeoutOptions, @@ -19,7 +19,14 @@ export interface FetchNodeOptionsToDir { storeDir: string fetchTimeout?: number nodeMirrorBaseUrl?: string + releaseChannel?: string retry?: RetryTimeoutOptions + /** + * The OpenPGP keys trusted to sign SHASUMS256.txt. Defaults to the Node.js + * release keys embedded in pnpm. A test seam only — not reachable from + * project config, so it cannot be used to weaken verification. + */ + trustedNodeReleaseKeys?: readonly ArmoredKey[] } export interface FetchNodeOptions { @@ -55,7 +62,17 @@ export async function fetchNode ( await validateSystemCompatibility() const nodeMirrorBaseUrl = opts.nodeMirrorBaseUrl ?? DEFAULT_NODE_MIRROR_BASE_URL - const artifactInfo = await getNodeArtifactInfo(fetch, version, { nodeMirrorBaseUrl }) + // The mirror is repository-configurable, so the SHASUMS file's hashes are + // only trustworthy once its OpenPGP signature is verified against the + // Node.js release keys embedded in pnpm. Only the `release` channel + // publishes a signed SHASUMS256.txt; pre-release channels (rc, nightly, …) + // are unsigned by Node, so they cannot be verified this way. + const verifyShasumsSignature = (opts.releaseChannel ?? 'release') === 'release' + const artifactInfo = await getNodeArtifactInfo(fetch, version, { + nodeMirrorBaseUrl, + verifyShasumsSignature, + trustedNodeReleaseKeys: opts.trustedNodeReleaseKeys, + }) if (artifactInfo.isZip) { await downloadAndUnpackZip(fetch, artifactInfo, targetDir) @@ -93,7 +110,9 @@ async function getNodeArtifactInfo ( version: string, opts: { nodeMirrorBaseUrl: string + verifyShasumsSignature: boolean integrities?: Record + trustedNodeReleaseKeys?: readonly ArmoredKey[] } ): Promise { const tarball = getNodeArtifactAddress({ @@ -109,7 +128,7 @@ async function getNodeArtifactInfo ( const integrity = opts.integrities ? opts.integrities[`${process.platform}-${process.arch}`] - : await loadArtifactIntegrity(fetch, tarballFileName, shasumsFileUrl) + : await loadArtifactIntegrity(fetch, tarballFileName, shasumsFileUrl, opts.verifyShasumsSignature, opts.trustedNodeReleaseKeys) return { url, @@ -132,9 +151,13 @@ async function getNodeArtifactInfo ( async function loadArtifactIntegrity ( fetch: FetchFromRegistry, fileName: string, - shasumsUrl: string + shasumsUrl: string, + verifySignature: boolean, + trustedNodeReleaseKeys?: readonly ArmoredKey[] ): Promise { - const body = await fetchShasumsFileRaw(fetch, shasumsUrl) + const body = verifySignature + ? await fetchVerifiedNodeShasums(fetch, shasumsUrl, trustedNodeReleaseKeys) + : await fetchShasumsFileRaw(fetch, shasumsUrl) return pickFileChecksumFromShasumsFile(body, fileName) } diff --git a/env/plugin-commands-env/package.json b/env/plugin-commands-env/package.json index 7840347f95..6397f787a3 100644 --- a/env/plugin-commands-env/package.json +++ b/env/plugin-commands-env/package.json @@ -68,6 +68,7 @@ "execa": "catalog:", "nock": "catalog:", "node-fetch": "catalog:", + "openpgp": "catalog:", "path-name": "catalog:", "tar-stream": "catalog:", "yazl": "catalog:" diff --git a/env/plugin-commands-env/src/downloadNodeVersion.ts b/env/plugin-commands-env/src/downloadNodeVersion.ts index 99059c5a92..28062d406d 100644 --- a/env/plugin-commands-env/src/downloadNodeVersion.ts +++ b/env/plugin-commands-env/src/downloadNodeVersion.ts @@ -26,7 +26,7 @@ export interface DownloadNodeVersionResult { export async function downloadNodeVersion (opts: NvmNodeCommandOptions, envSpecifier: string): Promise { const fetch = createFetchFromRegistry(opts) - const { nodeVersion, nodeMirrorBaseUrl } = await getNodeVersion(opts, envSpecifier) + const { nodeVersion, nodeMirrorBaseUrl, releaseChannel } = await getNodeVersion(opts, envSpecifier) if (!nodeVersion) { return null } @@ -34,6 +34,7 @@ export async function downloadNodeVersion (opts: NvmNodeCommandOptions, envSpeci ...opts, useNodeVersion: nodeVersion, nodeMirrorBaseUrl, + releaseChannel, }) globalInfo(`Node.js ${nodeVersion as string} was installed ${nodeDir}`) diff --git a/env/plugin-commands-env/src/node.ts b/env/plugin-commands-env/src/node.ts index 84da1bdc87..fa39bab91b 100644 --- a/env/plugin-commands-env/src/node.ts +++ b/env/plugin-commands-env/src/node.ts @@ -5,7 +5,7 @@ import { type Config } from '@pnpm/config' import { getSystemNodeVersion } from '@pnpm/env.system-node-version' import { createFetchFromRegistry, type FetchFromRegistry } from '@pnpm/fetch' import { globalInfo, globalWarn } from '@pnpm/logger' -import { fetchNode } from '@pnpm/node.fetcher' +import { fetchNode, type FetchNodeOptionsToDir } from '@pnpm/node.fetcher' import { getNodeMirror } from '@pnpm/node.resolver' import { getStorePath } from '@pnpm/store-path' import { type PrepareExecutionEnvOptions, type PrepareExecutionEnvResult } from '@pnpm/types' @@ -36,6 +36,8 @@ export type NvmNodeCommandOptions = Pick & Partial> & { remote?: boolean + /** See FetchNodeOptionsToDir.trustedNodeReleaseKeys — a test seam. */ + trustedNodeReleaseKeys?: FetchNodeOptionsToDir['trustedNodeReleaseKeys'] } const nodeFetchPromises: Record> = {} @@ -88,6 +90,7 @@ export async function getNodeBinDir (opts: NvmNodeCommandOptions): Promise { +export async function getNodeDir (fetch: FetchFromRegistry, opts: NvmNodeCommandOptions & { useNodeVersion: string, nodeMirrorBaseUrl: string, releaseChannel: string }): Promise { const nodesDir = getNodeVersionsBaseDir(opts.pnpmHomeDir) await fs.promises.mkdir(nodesDir, { recursive: true }) const versionDir = path.join(nodesDir, opts.useNodeVersion) diff --git a/env/plugin-commands-env/test/node.test.ts b/env/plugin-commands-env/test/node.test.ts index 163d0b1967..48f6991d8e 100644 --- a/env/plugin-commands-env/test/node.test.ts +++ b/env/plugin-commands-env/test/node.test.ts @@ -14,10 +14,9 @@ import { prepareExecutionEnv, } from '../lib/node.js' import { tempDir } from '@pnpm/prepare' +import * as openpgp from 'openpgp' -const fetchMock = jest.fn(async (url: string) => { - if (url.endsWith('SHASUMS256.txt')) { - return new Response(` +const SHASUMS_CONTENT = ` 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v16.4.0-darwin-arm64.tar.gz 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v16.4.0-linux-arm64.tar.gz 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v16.4.0-linux-x64.tar.gz @@ -26,7 +25,35 @@ a08f3386090e6511772b949d41970b75a6b71d28abb551dff9854ceb1929dae1 node-v16.4.0-w 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v18.0.0-rc.3-linux-arm64.tar.gz 5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef node-v18.0.0-rc.3-linux-x64.tar.gz 07e6121cba611b57f310a489f76c413b6246e79cffe1e9538b2478ffee11c99e node-v18.0.0-rc.3-win-x64.zip -`) +` + +// The SHASUMS fixture is signed with a test key generated in beforeAll, and the +// tests that hit the signed `release` channel trust that key via the +// trustedNodeReleaseKeys test seam. +let shasumsSignature!: Uint8Array +let trustedNodeReleaseKeys!: Array<{ armoredKey: string }> + +beforeAll(async () => { + const { privateKey, publicKey } = await openpgp.generateKey({ + userIDs: [{ name: 'Test Node Releaser', email: 'test@nodejs.example' }], + format: 'armored', + }) + const message = await openpgp.createMessage({ binary: new TextEncoder().encode(SHASUMS_CONTENT) }) + shasumsSignature = await openpgp.sign({ + message, + signingKeys: await openpgp.readPrivateKey({ armoredKey: privateKey }), + detached: true, + format: 'binary', + }) as Uint8Array + trustedNodeReleaseKeys = [{ armoredKey: publicKey }] +}) + +const fetchMock = jest.fn(async (url: string) => { + if (url.endsWith('SHASUMS256.txt.sig')) { + return new Response(Buffer.from(shasumsSignature)) + } + if (url.endsWith('SHASUMS256.txt')) { + return new Response(SHASUMS_CONTENT) } if (url.endsWith('.tar.gz')) { const pack = tar.pack() @@ -84,6 +111,7 @@ test('install Node uses node-mirror:release option', async () => { rawConfig: { 'node-mirror:release': nodeMirrorRelease, }, + trustedNodeReleaseKeys, useNodeVersion: '16.4.0', } @@ -133,6 +161,7 @@ test('specified an invalid Node.js via use-node-version should not cause pnpm it global: true, pnpmHomeDir: process.cwd(), rawConfig: {}, + trustedNodeReleaseKeys, useNodeVersion: '22.14', } diff --git a/package.json b/package.json index 82d2892745..e64fb87c01 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "scripts": { "bump": "changeset version && pnpm update-manifests", "changeset": "changeset", + "check:node-release-keys": "node crypto/shasums-file/scripts/update-node-release-keys.mjs", + "check:npm-signing-keys": "node tools/plugin-commands-self-updater/scripts/update-npm-signing-keys.mjs", + "update:node-release-keys": "node crypto/shasums-file/scripts/update-node-release-keys.mjs --update", "preinstall": "npx only-allow pnpm", "prepare": "husky", "pretest": "pnpm run compile-only && pnpm --dir=__fixtures__ run prepareFixtures", diff --git a/pkg-manager/package-bins/src/index.ts b/pkg-manager/package-bins/src/index.ts index 4fcaf6d249..9d7d8e53b8 100644 --- a/pkg-manager/package-bins/src/index.ts +++ b/pkg-manager/package-bins/src/index.ts @@ -49,7 +49,13 @@ function commandsFromBin (bin: PackageBin, pkgName: string, pkgPath: string): Co const binName = commandName[0] === '@' ? commandName.slice(commandName.indexOf('/') + 1) : commandName - // Validate: must be safe (no path traversal) - only allow URL-safe chars or $ + // Validate: must be safe (no path traversal). Reject empty and the + // filesystem-relative names "." and ".." (these survive encodeURIComponent + // unchanged but resolve to the bin directory itself or its parent when + // joined to a target dir), then only allow URL-safe chars or $. + if (binName === '' || binName === '.' || binName === '..') { + continue + } if (binName !== encodeURIComponent(binName) && binName !== '$') { continue } diff --git a/pkg-manager/package-bins/test/index.ts b/pkg-manager/package-bins/test/index.ts index f2777e2561..a7e41f463e 100644 --- a/pkg-manager/package-bins/test/index.ts +++ b/pkg-manager/package-bins/test/index.ts @@ -145,6 +145,26 @@ test('skip scoped bin names with path traversal', async () => { ]) }) +test('skip reserved bin names', async () => { + expect( + await getBinsFromPackageManifest({ + name: 'malicious', + version: '1.0.0', + bin: { + '': './empty.js', + '.': './dot.js', + '..': './dot-dot.js', + '@scope/..': './scoped-dot-dot.js', + good: './good', + }, + }, process.cwd())).toStrictEqual([ + { + name: 'good', + path: path.resolve('good'), + }, + ]) +}) + test('skip directories.bin with path traversal', async () => { // Security test: malicious packages can try to escape the package root // using directories.bin to chmod files at arbitrary locations diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ed4b44ac2..c0eb88627f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ catalogs: '@jest/globals': specifier: 29.7.0 version: 29.7.0 + '@openpgp/web-stream-tools': + specifier: 0.3.1 + version: 0.3.1 '@pnpm/byline': specifier: ^1.0.0 version: 1.0.0 @@ -528,6 +531,9 @@ catalogs: object-hash: specifier: 3.0.0 version: 3.0.0 + openpgp: + specifier: ^6.3.1 + version: 6.3.1 p-defer: specifier: ^3.0.0 version: 3.0.0 @@ -813,6 +819,7 @@ overrides: semver@<7.5.2: ^7.7.4 send@<0.19.0: ^0.19.0 serve-static@<1.16.0: ^1.16.0 + shell-quote@<1.8.4: '>=1.8.4' socks@2: ^2.8.1 tar@<=7.5.10: '>=7.5.11' tmp@<0.2.6: '>=0.2.6' @@ -1181,7 +1188,7 @@ importers: dependencies: '@pnpm/workspace.find-packages': specifier: 'catalog:' - version: 1000.0.65(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + version: 1000.0.65(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/workspace.read-manifest': specifier: 'catalog:' version: 1000.3.1 @@ -2083,7 +2090,13 @@ importers: '@pnpm/fetching-types': specifier: workspace:* version: link:../../network/fetching-types + openpgp: + specifier: 'catalog:' + version: 6.3.1(@openpgp/web-stream-tools@0.3.1(@types/node@25.6.0)(typescript@5.5.4)) devDependencies: + '@openpgp/web-stream-tools': + specifier: 'catalog:' + version: 0.3.1(@types/node@25.6.0)(typescript@5.5.4) '@pnpm/crypto.shasums-file': specifier: workspace:* version: 'link:' @@ -2452,6 +2465,9 @@ importers: node-fetch: specifier: 'catalog:' version: '@pnpm/node-fetch@1.0.0' + openpgp: + specifier: 'catalog:' + version: 6.3.1(@openpgp/web-stream-tools@0.3.1(@types/node@25.6.0)(typescript@5.5.4)) path-name: specifier: 'catalog:' version: 1.0.0 @@ -8646,18 +8662,36 @@ importers: '@pnpm/exec.pnpm-cli-runner': specifier: workspace:* version: link:../../exec/pnpm-cli-runner + '@pnpm/fetch': + specifier: workspace:* + version: link:../../network/fetch '@pnpm/link-bins': specifier: workspace:* version: link:../../pkg-manager/link-bins + '@pnpm/lockfile.fs': + specifier: workspace:* + version: link:../../lockfile/fs + '@pnpm/lockfile.types': + specifier: workspace:* + version: link:../../lockfile/types '@pnpm/logger': specifier: 'catalog:' version: 1001.0.1 + '@pnpm/network.auth-header': + specifier: workspace:* + version: link:../../network/auth-header + '@pnpm/pick-registry-for-package': + specifier: workspace:* + version: link:../../config/pick-registry-for-package '@pnpm/read-project-manifest': specifier: workspace:* version: link:../../pkg-manifest/read-project-manifest '@pnpm/tools.path': specifier: workspace:* version: link:../path + '@pnpm/types': + specifier: workspace:* + version: link:../../packages/types '@zkochan/rimraf': specifier: 'catalog:' version: 3.0.2 @@ -10024,6 +10058,18 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} + '@openpgp/web-stream-tools@0.3.1': + resolution: {integrity: sha512-EV+VQ4Dr8b+JmlGnc74FLgx7EhLyydOr4j6s6Hp+2scQh6sLQMs2h+1oEYUIslXcQPicWKG5ZQx+/dua0dgPWA==} + engines: {node: '>= 18.0.0'} + peerDependencies: + '@types/node': '>=18.0.0' + typescript: '>=4.7' + peerDependenciesMeta: + '@types/node': + optional: true + typescript: + optional: true + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -10540,6 +10586,12 @@ packages: peerDependencies: '@pnpm/logger': '>=1001.0.0 <1002.0.0' + '@pnpm/symlink-dependency@1000.0.18': + resolution: {integrity: sha512-GYSgIT7tT0yrVIAbd8i1G8YZkS8s+7mRCHendQz1iZ0QFPc4DKAD7TbOpe0XjWZbwBsSabYNfqaabz4fVe94ww==} + engines: {node: '>=18.12'} + peerDependencies: + '@pnpm/logger': ^1001.0.1 + '@pnpm/tabtab@0.5.4': resolution: {integrity: sha512-bWLDlHsBlgKY/05wDN/V3ETcn5G2SV/SiA2ZmNvKGGlmVX4G5li7GRDhHcgYvHJHyJ8TUStqg2xtHmCs0UbAbg==} engines: {node: '>=18'} @@ -10589,8 +10641,8 @@ packages: peerDependencies: '@pnpm/logger': ^1001.0.1 - '@pnpm/worker@1000.6.8': - resolution: {integrity: sha512-YddBChHihV4exQljLl2E7Aovjc84G3om2fxhr8IsZyUVY2EpHfaK31p5ClzvsRVh6urmWtb/Vo4U4l/1PePKlw==} + '@pnpm/worker@1000.6.9': + resolution: {integrity: sha512-5Ef0c7cvakujjwF+/UzOu+DRj9XE5Rb7TteVTd/iF8/tOqha+tPUQw/0rDb0AevP+h+BrfrGOCz4jewj8eaUnA==} engines: {node: '>=18.12'} peerDependencies: '@pnpm/logger': ^1001.0.1 @@ -14290,6 +14342,15 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + openpgp@6.3.1: + resolution: {integrity: sha512-7oSPvmlKPojxFoyelT5DWPIAVmqWZh4qU/5pO6bdoShEtRpCw9Sye9IXUQj6EFM3XpgGssqccAr705YtTcLNQw==} + engines: {node: '>= 18.0.0', typescript: '>= 5.0.0'} + peerDependencies: + '@openpgp/web-stream-tools': ~0.3.0 + peerDependenciesMeta: + '@openpgp/web-stream-tools': + optional: true + opt-cli@1.5.1: resolution: {integrity: sha512-iRFQBiQjXZ+LX/8pis04prUhS6FOYcJiZRouofN3rUJEB282b/e0s3jp9vT7aHgXY6TUpgPwu12f0i+qF40Kjw==} hasBin: true @@ -15041,8 +15102,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} shelljs@0.8.5: @@ -17196,6 +17257,11 @@ snapshots: dependencies: semver: 7.7.4 + '@openpgp/web-stream-tools@0.3.1(@types/node@25.6.0)(typescript@5.5.4)': + optionalDependencies: + '@types/node': 25.6.0 + typescript: 5.5.4 + '@pkgjs/parseargs@0.11.0': optional: true @@ -17237,11 +17303,11 @@ snapshots: - supports-color - typanion - '@pnpm/cli-utils@1001.3.10(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/cli-utils@1001.3.10(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/cli-meta': 1000.0.16 '@pnpm/config': 1004.11.0(@pnpm/logger@1001.0.1) - '@pnpm/config.deps-installer': 1000.1.5(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/config.deps-installer': 1000.1.5(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) '@pnpm/default-reporter': 1002.1.14(@pnpm/logger@1001.0.1) '@pnpm/error': 1000.1.0 '@pnpm/logger': 1001.0.1 @@ -17249,7 +17315,7 @@ snapshots: '@pnpm/package-is-installable': 1000.0.21(@pnpm/logger@1001.0.1) '@pnpm/pnpmfile': 1002.1.13(@pnpm/logger@1001.0.1) '@pnpm/read-project-manifest': 1001.2.6(@pnpm/logger@1001.0.1) - '@pnpm/store-connection-manager': 1002.3.19(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/store-connection-manager': 1002.3.19(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/types': 1001.3.0 '@pnpm/util.lex-comparator': 3.0.2 chalk: 4.1.2 @@ -17281,18 +17347,18 @@ snapshots: - supports-color - typanion - '@pnpm/client@1001.1.24(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/client@1001.1.24(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: - '@pnpm/default-resolver': 1002.3.8(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/default-resolver': 1002.3.8(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/directory-fetcher': 1000.1.24(@pnpm/logger@1001.0.1) '@pnpm/fetch': 1001.0.0(@pnpm/logger@1001.0.1) '@pnpm/fetching-types': 1000.2.1 - '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) - '@pnpm/git-fetcher': 1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/git-fetcher': 1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/network.auth-header': 1000.0.7 - '@pnpm/node.fetcher': 1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/node.fetcher': 1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/resolver-base': 1005.4.1 - '@pnpm/tarball-fetcher': 1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/tarball-fetcher': 1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/types': 1001.3.0 ramda: '@pnpm/ramda@0.28.1' transitivePeerDependencies: @@ -17337,7 +17403,7 @@ snapshots: - domexception - supports-color - '@pnpm/config.deps-installer@1000.1.5(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': + '@pnpm/config.deps-installer@1000.1.5(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': dependencies: '@pnpm/config.config-writer': 1000.1.3(@pnpm/logger@1001.0.1) '@pnpm/core-loggers': 1001.0.9(@pnpm/logger@1001.0.1) @@ -17346,7 +17412,7 @@ snapshots: '@pnpm/logger': 1001.0.1 '@pnpm/network.auth-header': 1000.0.7 '@pnpm/npm-resolver': 1005.2.3(@pnpm/logger@1001.0.1) - '@pnpm/package-store': 1007.1.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/package-store': 1007.1.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) '@pnpm/parse-wanted-dependency': 1001.0.0 '@pnpm/pick-registry-for-package': 1000.0.16 '@pnpm/read-modules-dir': 1000.0.0 @@ -17495,7 +17561,7 @@ snapshots: - supports-color - typanion - '@pnpm/default-resolver@1002.3.8(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/default-resolver@1002.3.8(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/error': 1000.1.0 '@pnpm/fetching-types': 1000.2.1 @@ -17504,8 +17570,8 @@ snapshots: '@pnpm/node.resolver': 1001.0.23(@pnpm/logger@1001.0.1) '@pnpm/npm-resolver': 1005.2.3(@pnpm/logger@1001.0.1) '@pnpm/resolver-base': 1005.4.1 - '@pnpm/resolving.bun-resolver': 1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) - '@pnpm/resolving.deno-resolver': 1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/resolving.bun-resolver': 1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/resolving.deno-resolver': 1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/tarball-resolver': 1002.2.1 transitivePeerDependencies: - '@pnpm/logger' @@ -17596,12 +17662,12 @@ snapshots: transitivePeerDependencies: - domexception - '@pnpm/fetching.binary-fetcher@1005.0.4(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': + '@pnpm/fetching.binary-fetcher@1005.0.4(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': dependencies: '@pnpm/error': 1000.1.0 '@pnpm/fetcher-base': 1001.2.2 '@pnpm/fetching-types': 1000.2.1 - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) adm-zip: 0.5.17 is-subdir: 1.2.0 rename-overwrite: 6.0.6 @@ -17680,14 +17746,14 @@ snapshots: - supports-color - typanion - '@pnpm/git-fetcher@1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/git-fetcher@1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/error': 1000.1.0 '@pnpm/fetcher-base': 1001.2.2 '@pnpm/fs.packlist': 1000.0.0 '@pnpm/logger': 1001.0.1 '@pnpm/prepare-package': 1001.0.7(@pnpm/logger@1001.0.1)(typanion@3.14.0) - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) '@zkochan/rimraf': 3.0.2 execa: safe-execa@0.1.2 transitivePeerDependencies: @@ -17876,15 +17942,15 @@ snapshots: - supports-color - typanion - '@pnpm/node.fetcher@1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/node.fetcher@1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/create-cafs-store': 1000.0.33(@pnpm/logger@1001.0.1) '@pnpm/crypto.shasums-file': 1001.0.5 '@pnpm/error': 1000.1.0 '@pnpm/fetching-types': 1000.2.1 - '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) '@pnpm/node.resolver': 1001.0.23(@pnpm/logger@1001.0.1) - '@pnpm/tarball-fetcher': 1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/tarball-fetcher': 1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) detect-libc: 2.1.2 transitivePeerDependencies: - '@pnpm/logger' @@ -18039,7 +18105,7 @@ snapshots: semver: 7.7.4 ssri: 10.0.5 - '@pnpm/package-requester@1011.2.4(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': + '@pnpm/package-requester@1011.2.4(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': dependencies: '@pnpm/core-loggers': 1001.0.9(@pnpm/logger@1001.0.1) '@pnpm/dependency-path': 1001.1.10 @@ -18054,7 +18120,7 @@ snapshots: '@pnpm/store-controller-types': 1004.5.1 '@pnpm/store.cafs': 1000.1.4 '@pnpm/types': 1001.3.0 - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) detect-libc: 2.1.2 p-defer: 3.0.0 p-limit: 3.1.0 @@ -18084,19 +18150,19 @@ snapshots: ssri: 10.0.5 symlink-dir: 6.0.5 - '@pnpm/package-store@1007.1.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': + '@pnpm/package-store@1007.1.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))': dependencies: '@pnpm/create-cafs-store': 1000.0.33(@pnpm/logger@1001.0.1) '@pnpm/crypto.hash': 1000.2.2 '@pnpm/error': 1000.1.0 '@pnpm/fetcher-base': 1001.2.2 '@pnpm/logger': 1001.0.1 - '@pnpm/package-requester': 1011.2.4(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/package-requester': 1011.2.4(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) '@pnpm/resolver-base': 1005.4.1 '@pnpm/store-controller-types': 1004.5.1 '@pnpm/store.cafs': 1000.1.4 '@pnpm/types': 1001.3.0 - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) '@zkochan/rimraf': 3.0.2 is-subdir: 1.2.0 load-json-file: 6.2.0 @@ -18253,20 +18319,20 @@ snapshots: - supports-color - typanion - '@pnpm/resolving.bun-resolver@1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/resolving.bun-resolver@1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/constants': 1001.3.1 '@pnpm/crypto.shasums-file': 1001.0.5 '@pnpm/error': 1000.1.0 '@pnpm/fetcher-base': 1001.2.2 '@pnpm/fetching-types': 1000.2.1 - '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) - '@pnpm/node.fetcher': 1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/node.fetcher': 1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/npm-resolver': 1005.2.3(@pnpm/logger@1001.0.1) '@pnpm/resolver-base': 1005.4.1 '@pnpm/types': 1001.3.0 '@pnpm/util.lex-comparator': 3.0.2 - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) semver: 7.7.4 transitivePeerDependencies: - '@pnpm/logger' @@ -18295,20 +18361,20 @@ snapshots: - supports-color - typanion - '@pnpm/resolving.deno-resolver@1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/resolving.deno-resolver@1005.0.11(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/constants': 1001.3.1 '@pnpm/crypto.shasums-file': 1001.0.5 '@pnpm/error': 1000.1.0 '@pnpm/fetcher-base': 1001.2.2 '@pnpm/fetching-types': 1000.2.1 - '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) - '@pnpm/node.fetcher': 1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/fetching.binary-fetcher': 1005.0.4(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/node.fetcher': 1001.0.27(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/npm-resolver': 1005.2.3(@pnpm/logger@1001.0.1) '@pnpm/resolver-base': 1005.4.1 '@pnpm/types': 1001.3.0 '@pnpm/util.lex-comparator': 3.0.2 - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) semver: 7.7.4 transitivePeerDependencies: - '@pnpm/logger' @@ -18364,14 +18430,14 @@ snapshots: - supports-color - typanion - '@pnpm/store-connection-manager@1002.3.19(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/store-connection-manager@1002.3.19(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/cli-meta': 1000.0.16 - '@pnpm/client': 1001.1.24(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/client': 1001.1.24(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/config': 1004.11.0(@pnpm/logger@1001.0.1) '@pnpm/error': 1000.1.0 '@pnpm/logger': 1001.0.1 - '@pnpm/package-store': 1007.1.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) + '@pnpm/package-store': 1007.1.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)) '@pnpm/server': 1001.0.20(@pnpm/logger@1001.0.1) '@pnpm/store-path': 1000.0.6 '@zkochan/diable': 1.0.2 @@ -18439,6 +18505,13 @@ snapshots: '@pnpm/types': 1001.3.0 symlink-dir: 6.0.5 + '@pnpm/symlink-dependency@1000.0.18(@pnpm/logger@1001.0.1)': + dependencies: + '@pnpm/core-loggers': 1001.0.9(@pnpm/logger@1001.0.1) + '@pnpm/logger': 1001.0.1 + '@pnpm/types': 1001.3.0 + symlink-dir: 6.0.5 + '@pnpm/tabtab@0.5.4': dependencies: debug: 4.4.3 @@ -18471,7 +18544,7 @@ snapshots: - supports-color - typanion - '@pnpm/tarball-fetcher@1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/tarball-fetcher@1006.0.6(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: '@pnpm/core-loggers': 1001.0.9(@pnpm/logger@1001.0.1) '@pnpm/error': 1000.1.0 @@ -18482,7 +18555,7 @@ snapshots: '@pnpm/logger': 1001.0.1 '@pnpm/prepare-package': 1001.0.7(@pnpm/logger@1001.0.1)(typanion@3.14.0) '@pnpm/types': 1001.3.0 - '@pnpm/worker': 1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0) + '@pnpm/worker': 1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0) '@zkochan/retry': 0.2.0 lodash.throttle: 4.1.1 p-map-values: 1.0.0 @@ -18538,7 +18611,7 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0)': + '@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0)': dependencies: '@pnpm/cafs-types': 1000.1.0 '@pnpm/create-cafs-store': 1000.0.34(@pnpm/logger@1001.0.1) @@ -18549,7 +18622,7 @@ snapshots: '@pnpm/graceful-fs': 1000.1.0 '@pnpm/logger': 1001.0.1 '@pnpm/store.cafs': 1000.1.5 - '@pnpm/symlink-dependency': 1000.0.17(@pnpm/logger@1001.0.1) + '@pnpm/symlink-dependency': 1000.0.18(@pnpm/logger@1001.0.1) '@rushstack/worker-pool': 0.4.9(@types/node@25.6.0) is-windows: 1.0.2 load-json-file: 6.2.0 @@ -18571,9 +18644,9 @@ snapshots: - supports-color - typanion - '@pnpm/workspace.find-packages@1000.0.65(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': + '@pnpm/workspace.find-packages@1000.0.65(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0)': dependencies: - '@pnpm/cli-utils': 1001.3.10(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.8(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) + '@pnpm/cli-utils': 1001.3.10(@pnpm/logger@1001.0.1)(@pnpm/worker@1000.6.9(@pnpm/logger@1001.0.1)(@types/node@25.6.0))(typanion@3.14.0) '@pnpm/constants': 1001.3.1 '@pnpm/fs.find-packages': 1000.0.24(@pnpm/logger@1001.0.1) '@pnpm/logger': 1001.0.1 @@ -20002,7 +20075,7 @@ snapshots: date-fns: 2.30.0 lodash: 4.18.1 rxjs: 7.8.2 - shell-quote: 1.8.3 + shell-quote: 1.8.4 spawn-command: 0.0.2 supports-color: 8.1.1 tree-kill: 1.2.2 @@ -22925,6 +22998,10 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openpgp@6.3.1(@openpgp/web-stream-tools@0.3.1(@types/node@25.6.0)(typescript@5.5.4)): + optionalDependencies: + '@openpgp/web-stream-tools': 0.3.1(@types/node@25.6.0)(typescript@5.5.4) + opt-cli@1.5.1: dependencies: commander: 2.9.0 @@ -23720,7 +23797,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.3: {} + shell-quote@1.8.4: {} shelljs@0.8.5: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d915aec8e0..ca1f1aa278 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -237,6 +237,8 @@ catalog: normalize-registry-url: 2.0.1 npm-packlist: 5.1.3 object-hash: 3.0.0 + openpgp: ^6.3.1 + '@openpgp/web-stream-tools': 0.3.1 p-defer: ^3.0.0 p-every: ^2.0.0 p-filter: ^2.1.0 @@ -395,6 +397,7 @@ overrides: semver@<7.5.2: 'catalog:' send@<0.19.0: ^0.19.0 serve-static@<1.16.0: ^1.16.0 + shell-quote@<1.8.4: '>=1.8.4' socks@2: ^2.8.1 tar@<=7.5.10: '>=7.5.11' tmp@<0.2.6: '>=0.2.6' diff --git a/pnpm/package.json b/pnpm/package.json index 505521f08c..072b3a2f41 100644 --- a/pnpm/package.json +++ b/pnpm/package.json @@ -218,6 +218,7 @@ "semver@<7.5.2": "^7.7.4", "send@<0.19.0": "^0.19.0", "serve-static@<1.16.0": "^1.16.0", + "shell-quote@<1.8.4": ">=1.8.4", "socks@2": "^2.8.1", "tar@<=7.5.10": ">=7.5.11", "tmp@<0.2.6": ">=0.2.6", diff --git a/pnpm/src/packageManagerRegistries.ts b/pnpm/src/packageManagerRegistries.ts new file mode 100644 index 0000000000..3f69c93641 --- /dev/null +++ b/pnpm/src/packageManagerRegistries.ts @@ -0,0 +1,28 @@ +import { type Config, type PackageManagerNetworkConfig } from '@pnpm/config' +import { type Registries } from '@pnpm/types' + +const DEFAULT_PACKAGE_MANAGER_REGISTRY = 'https://registry.npmjs.org/' + +export type PackageManagerBootstrapConfig = Partial & { + registries: Registries +} + +/** + * The registries used to download and verify a package-manager binary. These + * are built from trusted config sources only (CLI options, env config, user + * and global .npmrc), defaulting to the public npm registry — repository + * config must not steer where pnpm fetches the binary it is about to execute. + */ +export function getPackageManagerRegistries (config: Config): Registries { + return { + default: DEFAULT_PACKAGE_MANAGER_REGISTRY, + ...config.packageManagerRegistries, + } +} + +export function getPackageManagerBootstrapConfig (config: Config): PackageManagerBootstrapConfig { + return { + ...config.packageManagerNetworkConfig, + registries: getPackageManagerRegistries(config), + } +} diff --git a/pnpm/src/switchCliVersion.ts b/pnpm/src/switchCliVersion.ts index 5c05b20dc5..25a348a16c 100644 --- a/pnpm/src/switchCliVersion.ts +++ b/pnpm/src/switchCliVersion.ts @@ -8,6 +8,8 @@ import { installPnpmToTools } from '@pnpm/tools.plugin-commands-self-updater' import spawn from 'cross-spawn' import semver from 'semver' +import { getPackageManagerBootstrapConfig } from './packageManagerRegistries.js' + export async function switchCliVersion (config: Config): Promise { const pm = config.wantedPackageManager if (pm == null || pm.name !== 'pnpm' || pm.version == null || pm.version === packageManager.version) return @@ -20,7 +22,12 @@ export async function switchCliVersion (config: Config): Promise { globalWarn(`Cannot switch to pnpm@${pm.version}: you need to specify the version as "${pmVersion}"`) return } - const { binDir: wantedPnpmBinDir } = await installPnpmToTools(pmVersion, config) + // Bootstrap traffic for the wanted pnpm version must not be steered by + // repository-controlled registry/proxy/TLS settings. + const { binDir: wantedPnpmBinDir } = await installPnpmToTools(pmVersion, { + ...config, + ...getPackageManagerBootstrapConfig(config), + }) const pnpmEnv = prependDirsToPath([wantedPnpmBinDir]) if (!pnpmEnv.updated) { // We throw this error to prevent an infinite recursive call of the same pnpm version. diff --git a/tools/plugin-commands-self-updater/package.json b/tools/plugin-commands-self-updater/package.json index 7d2091b659..9009b9f1fb 100644 --- a/tools/plugin-commands-self-updater/package.json +++ b/tools/plugin-commands-self-updater/package.json @@ -37,9 +37,15 @@ "@pnpm/config": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/exec.pnpm-cli-runner": "workspace:*", + "@pnpm/fetch": "workspace:*", "@pnpm/link-bins": "workspace:*", + "@pnpm/lockfile.fs": "workspace:*", + "@pnpm/lockfile.types": "workspace:*", + "@pnpm/network.auth-header": "workspace:*", + "@pnpm/pick-registry-for-package": "workspace:*", "@pnpm/read-project-manifest": "workspace:*", "@pnpm/tools.path": "workspace:*", + "@pnpm/types": "workspace:*", "@zkochan/rimraf": "catalog:", "path-temp": "catalog:", "ramda": "catalog:", diff --git a/tools/plugin-commands-self-updater/scripts/update-npm-signing-keys.mjs b/tools/plugin-commands-self-updater/scripts/update-npm-signing-keys.mjs new file mode 100644 index 0000000000..98568ae3f9 --- /dev/null +++ b/tools/plugin-commands-self-updater/scripts/update-npm-signing-keys.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +// Keeps the embedded npm registry signing keys (src/npmSigningKeys.ts) in sync +// with https://registry.npmjs.org/-/npm/v1/keys. +// +// node update-npm-signing-keys.mjs # check (CI / release gate) +// node update-npm-signing-keys.mjs --update # rewrite the embedded keys +// +// `--check` fails when npm advertises a signing key that is not embedded +// verbatim, so a key rotation cannot silently break (or weaken) pnpm's +// signature verification. `--update` writes the union of npm's keys and any +// embedded keys npm no longer lists (older keys are kept so packages published +// before a rotation still verify). +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const KEYS_URL = 'https://registry.npmjs.org/-/npm/v1/keys' +const KEYS_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'npmSigningKeys.ts') +const KEY_FIELDS = ['expires', 'keyid', 'keytype', 'scheme', 'key'] + +async function main () { + const update = process.argv.includes('--update') + const npmKeys = await fetchNpmKeys() + const embedded = readEmbeddedKeys() + + const missing = npmKeys.filter((npmKey) => !embedded.some((e) => keysEqual(e, npmKey))) + + if (!update) { + if (missing.length === 0) { + console.log(`✓ Embedded npm signing keys are up to date (${embedded.length} key(s)).`) + return + } + console.error(`✗ Embedded npm signing keys are out of date. ${missing.length} key(s) advertised by npm are not embedded:`) + for (const key of missing) console.error(` - ${key.keyid}`) + console.error(`\nRun: node ${path.relative(process.cwd(), fileURLToPath(import.meta.url))} --update`) + process.exit(1) + } + + // Union: every npm key, plus embedded keys npm no longer lists (kept for + // verifying packages published before a rotation). + const merged = [...npmKeys] + for (const e of embedded) { + if (!merged.some((m) => m.keyid === e.keyid)) merged.push(e) + } + fs.writeFileSync(KEYS_FILE, render(merged)) + console.log(missing.length === 0 + ? '✓ Embedded npm signing keys already current; rewrote file.' + : `✓ Updated embedded npm signing keys (added ${missing.length}).`) +} + +async function fetchNpmKeys () { + const res = await fetch(KEYS_URL) + if (!res.ok) throw new Error(`Failed to fetch ${KEYS_URL}: ${res.status}`) + const body = await res.json() + if (!Array.isArray(body?.keys)) throw new Error(`Unexpected response from ${KEYS_URL}`) + return body.keys.map(pickFields) +} + +function readEmbeddedKeys () { + const source = fs.readFileSync(KEYS_FILE, 'utf8') + const start = source.indexOf('[', source.indexOf('NPM_SIGNING_KEYS')) + if (start === -1) throw new Error(`Could not find NPM_SIGNING_KEYS array in ${KEYS_FILE}`) + let depth = 0 + let end = -1 + for (let i = start; i < source.length; i++) { + if (source[i] === '[') depth++ + else if (source[i] === ']' && --depth === 0) { end = i + 1; break } + } + if (end === -1) throw new Error(`Unterminated NPM_SIGNING_KEYS array in ${KEYS_FILE}`) + return JSON.parse(source.slice(start, end)).map(pickFields) +} + +function pickFields (key) { + const out = {} + for (const f of KEY_FIELDS) out[f] = key[f] ?? null + return out +} + +function keysEqual (a, b) { + return KEY_FIELDS.every((f) => (a[f] ?? null) === (b[f] ?? null)) +} + +function render (keys) { + const body = JSON.stringify(keys, KEY_FIELDS, 2) + return `/* eslint-disable */ +// GENERATED — npm's public registry signing keys, mirrored from +// ${KEYS_URL} +// +// Refresh with: node deps/security/signatures/scripts/update-npm-signing-keys.mjs --update +// The release workflow runs \`--check\` and fails if these drift from npm, so a +// rotated key cannot silently break (or weaken) signature verification. +export const NPM_SIGNING_KEYS = ${body} as const satisfies ReadonlyArray<{ + expires: string | null + keyid: string + keytype: string + scheme: string + key: string +}> +` +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/tools/plugin-commands-self-updater/src/index.ts b/tools/plugin-commands-self-updater/src/index.ts index 0d1a021c87..036ff678d8 100644 --- a/tools/plugin-commands-self-updater/src/index.ts +++ b/tools/plugin-commands-self-updater/src/index.ts @@ -1,4 +1,5 @@ import * as selfUpdate from './selfUpdate.js' export { installPnpmToTools } from './installPnpmToTools.js' +export { getNpmSigningKeys, verifyPnpmEngineIdentity, type RegistryKey, type VerifyPnpmEngineIdentityOptions } from './verifyPnpmEngineIdentity.js' export { selfUpdate } diff --git a/tools/plugin-commands-self-updater/src/installPnpmToTools.ts b/tools/plugin-commands-self-updater/src/installPnpmToTools.ts index 10e4a107da..968ca40a35 100644 --- a/tools/plugin-commands-self-updater/src/installPnpmToTools.ts +++ b/tools/plugin-commands-self-updater/src/installPnpmToTools.ts @@ -9,6 +9,7 @@ import { fastPathTemp as pathTemp } from 'path-temp' import semver from 'semver' import symlinkDir from 'symlink-dir' import { type SelfUpdateCommandOptions } from './selfUpdate.js' +import { verifyPnpmEngineIdentity } from './verifyPnpmEngineIdentity.js' export interface InstallPnpmToToolsResult { binDir: string @@ -98,6 +99,11 @@ export async function installPnpmToTools (pnpmVersion: string, opts: SelfUpdateC if (targetPkgName === '@pnpm/exe') { linkExePlatformBinary(stage) } + // Reached only when the wanted version is not yet in the tools directory + // (an actual download), so the signature check does not run on every + // invocation. Verify before the staged install is linked into place and + // spawned — on failure the stage is removed by the catch below. + await verifyPnpmEngineIdentity(stage, targetPkgName, pnpmVersion, opts) // We need the operation of installing pnpm to be atomic. // However, we cannot use a rename as that breaks the command shim created for pnpm. // Hence, we use a symlink. diff --git a/tools/plugin-commands-self-updater/src/npmSigningKeys.ts b/tools/plugin-commands-self-updater/src/npmSigningKeys.ts new file mode 100644 index 0000000000..c3a645e0e3 --- /dev/null +++ b/tools/plugin-commands-self-updater/src/npmSigningKeys.ts @@ -0,0 +1,29 @@ +/* eslint-disable */ +// GENERATED — npm's public registry signing keys, mirrored from +// https://registry.npmjs.org/-/npm/v1/keys +// +// Refresh with: node tools/plugin-commands-self-updater/scripts/update-npm-signing-keys.mjs --update +// The release workflow runs `--check` and fails if these drift from npm, so a +// rotated key cannot silently break (or weaken) signature verification. +export const NPM_SIGNING_KEYS = [ + { + "expires": "2025-01-29T00:00:00.000Z", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==" + }, + { + "expires": null, + "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U", + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==" + } +] as const satisfies ReadonlyArray<{ + expires: string | null + keyid: string + keytype: string + scheme: string + key: string +}> diff --git a/tools/plugin-commands-self-updater/src/selfUpdate.ts b/tools/plugin-commands-self-updater/src/selfUpdate.ts index 0afc246faf..3982143d57 100644 --- a/tools/plugin-commands-self-updater/src/selfUpdate.ts +++ b/tools/plugin-commands-self-updater/src/selfUpdate.ts @@ -11,6 +11,7 @@ import pick from 'ramda/src/pick' import renderHelp from 'render-help' import semver from 'semver' import { installPnpmToTools } from './installPnpmToTools.js' +import { type VerifyPnpmEngineIdentityOptions } from './verifyPnpmEngineIdentity.js' export function rcOptionsTypes (): Record { return pick([], allTypes) @@ -58,7 +59,10 @@ export type SelfUpdateCommandOptions = Pick +> & { + /** See {@link VerifyPnpmEngineIdentityOptions.trustedKeys} — a test seam. */ + trustedKeys?: VerifyPnpmEngineIdentityOptions['trustedKeys'] +} export async function handler ( opts: SelfUpdateCommandOptions, diff --git a/tools/plugin-commands-self-updater/src/verifyPnpmEngineIdentity.ts b/tools/plugin-commands-self-updater/src/verifyPnpmEngineIdentity.ts new file mode 100644 index 0000000000..ba670ca7fe --- /dev/null +++ b/tools/plugin-commands-self-updater/src/verifyPnpmEngineIdentity.ts @@ -0,0 +1,362 @@ +import crypto from 'crypto' +import fs from 'fs' +import path from 'path' +import url from 'url' +import util from 'util' + +import { PnpmError } from '@pnpm/error' +import { createFetchFromRegistry, type CreateFetchFromRegistryOptions } from '@pnpm/fetch' +import { type LockfileObject } from '@pnpm/lockfile.types' +import { readWantedLockfile } from '@pnpm/lockfile.fs' +import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header' +import { pickRegistryForPackage } from '@pnpm/pick-registry-for-package' +import { type DepPath, type Registries } from '@pnpm/types' + +import { NPM_SIGNING_KEYS } from './npmSigningKeys.js' + +export interface RegistryKey { + expires: string | null + key: string + keyid: string + keytype: string + scheme: string +} + +/** + * The trusted npm signing keys used to verify package-manager binaries before + * pnpm spawns them — npm's public keys embedded in the CLI. There is + * deliberately no way to override or disable them at runtime: a verification + * off-switch would be a footgun, and npm mirrors work without one (they proxy + * the same signed packument, which is verified against these keys). The keys + * are refreshed at release time by the update-npm-signing-keys script. + */ +export function getNpmSigningKeys (): RegistryKey[] { + return NPM_SIGNING_KEYS.map((k) => ({ ...k })) +} + +export interface VerifyPnpmEngineIdentityOptions extends CreateFetchFromRegistryOptions { + registries: Registries + rawConfig: Record + retry?: { retries?: number } + timeout?: number + /** + * The npm signing keys to trust. Defaults to {@link getNpmSigningKeys} (npm's + * embedded public keys). A test seam only — passing an empty array skips + * verification. Not reachable from project config, so it cannot be used to + * weaken verification for a real install. + */ + trustedKeys?: RegistryKey[] +} + +interface InstalledPackageToVerify { + name: string + /** The registry the package was installed from — the packument (and its signatures) is fetched from here. */ + registry: string + version: string + /** Integrity of the bytes actually installed on disk (from the staged lockfile). */ + integrity: string +} + +/** + * Verifies that the pnpm engine staged at `stageDir` (and about to be linked + * into the tools directory and executed) is genuinely the published `pnpm` / + * `@pnpm/exe` — i.e. the bytes recorded in the staged lockfile carry a valid + * npm registry signature for their exact `name@version`. + * + * The wanted pnpm version comes from a repository's `packageManager` field, + * so without this check a cloned repository could make pnpm download and run + * an arbitrary native binary. Signatures are verified against npm's embedded + * public keys (see {@link getNpmSigningKeys}), so a registry cannot answer + * with its own key pair; the signed packument is fetched from the configured + * registry, which an npm mirror proxies transparently. + * + * Fails closed: verification failure — including an unreachable registry — + * refuses the version switch rather than running an unverified binary. This + * runs only when the engine is actually being installed (a tools-directory + * cache miss), so it does not add a network round trip to every command. + */ +export async function verifyPnpmEngineIdentity ( + stageDir: string, + targetPkgName: string, + pnpmVersion: string, + opts: VerifyPnpmEngineIdentityOptions +): Promise { + const trustedKeys = opts.trustedKeys ?? getNpmSigningKeys() + if (trustedKeys.length === 0) return // test seam: no trusted keys means skip + + const lockfile = await readWantedLockfile(stageDir, { ignoreIncompatible: true }) + if (lockfile == null) { + throw new PnpmError( + 'PNPM_ENGINE_IDENTITY_UNVERIFIABLE', + `Cannot verify the identity of pnpm@${pnpmVersion}: the staged install has no lockfile.` + ) + } + const toVerify = collectEnginePackagesToVerify(lockfile, stageDir, targetPkgName, pnpmVersion, opts.registries) + + const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.rawConfig }) + const fetchFromRegistry = createFetchFromRegistry(opts) + + const failures: EngineSignatureFailure[] = [] + await Promise.all(toVerify.map(async (pkg) => { + const failure = await findSignatureFailure(pkg, trustedKeys, { fetchFromRegistry, getAuthHeader, retry: opts.retry, timeout: opts.timeout }) + if (failure != null) { + failures.push({ name: pkg.name, version: pkg.version, ...failure }) + } + })) + if (failures.length === 0) return + + failures.sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`)) + const onlyUnreachable = failures.every((f) => f.category === 'unreachable') + throw new PnpmError( + onlyUnreachable ? 'PNPM_ENGINE_IDENTITY_UNVERIFIABLE' : 'PNPM_ENGINE_IDENTITY_MISMATCH', + `Refusing to run pnpm@${pnpmVersion}: its npm registry signature could not be verified ` + + `(${failures.map(({ name, version, reason }) => `${name}@${version}: ${reason}`).join('; ')}). ` + + 'The bytes selected by this install do not match a published, signed pnpm release.', + { hint: 'This can indicate a tampered download or a malicious/unreachable registry. Set `manage-package-manager-versions` to `false` to skip the version switch if this is unexpected.' } + ) +} + +/** + * Why a package failed signature verification: + * - `invalid`: a registry signature is present but does not validate over the + * installed bytes — a strong tamper signal. + * - `absent`: the package/version is not on the registry, or carries no + * signature — suspicious for a package that is expected to be signed. + * - `unreachable`: the trust root could not be consulted (the network request + * failed) — typically transient/offline, not evidence of tampering. + */ +type SignatureFailureCategory = 'invalid' | 'absent' | 'unreachable' + +interface EngineSignatureFailure { + name: string + version: string + reason: string + category: SignatureFailureCategory +} + +function collectEnginePackagesToVerify ( + lockfile: LockfileObject, + stageDir: string, + targetPkgName: string, + version: string, + registries: Registries +): InstalledPackageToVerify[] { + const toVerify = [engineComponentToVerify(lockfile, registries, targetPkgName, version)] + if (targetPkgName === '@pnpm/exe') { + // The bytes actually executed are the host's platform binary, listed as an + // optional dependency of `@pnpm/exe`. Verify every platform package the + // staged install actually materialized on disk. + const optionalDeps = lockfile.packages?.[`@pnpm/exe@${version}` as DepPath]?.optionalDependencies ?? {} + for (const [name, platformVersion] of Object.entries(optionalDeps)) { + if (!fs.existsSync(path.join(stageDir, 'node_modules', name))) continue + toVerify.push(engineComponentToVerify(lockfile, registries, name, platformVersion)) + } + } + return toVerify +} + +function engineComponentToVerify ( + lockfile: LockfileObject, + registries: Registries, + name: string, + version: string +): InstalledPackageToVerify { + const resolution = lockfile.packages?.[`${name}@${version}` as DepPath]?.resolution + const integrity = (resolution as { integrity?: unknown } | undefined)?.integrity + if (typeof integrity !== 'string' || !integrity) { + // pnpm can install a tarball without integrity, so a missing integrity must + // fail closed rather than silently exempt that component from verification. + throw new PnpmError( + 'PNPM_ENGINE_IDENTITY_UNVERIFIABLE', + `Cannot verify the identity of ${name}@${version}: its integrity metadata is missing from the staged lockfile.` + ) + } + return { name, version, registry: pickRegistryForPackage(registries, name), integrity } +} + +interface PackageSignature { + keyid: string + sig: string +} + +interface PackumentVersion { + dist?: { + integrity?: string + signatures?: unknown + tarball?: string + } +} + +interface Packument { + time?: Record + versions?: Record +} + +interface FetchPackumentContext { + fetchFromRegistry: ReturnType + getAuthHeader: (uri: string) => string | undefined + retry?: { retries?: number } + timeout?: number +} + +async function findSignatureFailure ( + pkg: InstalledPackageToVerify, + trustedKeys: RegistryKey[], + ctx: FetchPackumentContext +): Promise<{ reason: string, category: SignatureFailureCategory } | undefined> { + let packument: Packument | undefined + try { + packument = await fetchPackument(pkg, ctx) + } catch (err: unknown) { + // Fetch-layer errors embed the request URL, which may carry credentials. + return { reason: redactTextCredentials(util.types.isNativeError(err) ? err.message : String(err)), category: 'unreachable' } + } + if (!packument) return { reason: `${pkg.name} is not published on ${redactUrlCredentials(pkg.registry)}`, category: 'absent' } + + const version = packument.versions?.[pkg.version] + if (!version) return { reason: `${pkg.name}@${pkg.version} was not found on ${redactUrlCredentials(pkg.registry)}`, category: 'absent' } + + const rawSignatures = version.dist?.signatures + if (rawSignatures != null && !Array.isArray(rawSignatures)) { + return { reason: `malformed registry signatures metadata for ${pkg.name}@${pkg.version}`, category: 'absent' } + } + const signatures = rawSignatures ?? [] + if (!signatures.every(isPackageSignature)) { + return { reason: `malformed registry signatures metadata for ${pkg.name}@${pkg.version}`, category: 'absent' } + } + if (signatures.length === 0) { + return { reason: `${pkg.name}@${pkg.version} has no registry signature`, category: 'absent' } + } + + // The message is built from the installed integrity, so a signature only + // validates when the installed bytes match what the registry signed. + return verifyPackageSignatures(pkg, packument.time?.[pkg.version], signatures, trustedKeys) +} + +async function fetchPackument ( + pkg: InstalledPackageToVerify, + ctx: FetchPackumentContext +): Promise { + const registryUrl = pkg.registry.endsWith('/') ? pkg.registry : `${pkg.registry}/` + const packumentUrl = toUri(pkg.name, registryUrl) + + const response = await ctx.fetchFromRegistry(packumentUrl, { + authHeaderValue: ctx.getAuthHeader(registryUrl), + fullMetadata: true, + retry: ctx.retry, + timeout: ctx.timeout, + }) + + if (response.status === 404) { + return undefined + } + if (response.status !== 200) { + throw new PnpmError( + 'ENGINE_IDENTITY_PACKUMENT_FETCH_FAIL', + `The packument endpoint (at ${redactUrlCredentials(packumentUrl)}) responded with ${response.status}: ${(await response.text()).slice(0, 500)}` + ) + } + + const body: unknown = await response.json() + if (!isPackument(body)) { + throw new PnpmError( + 'ENGINE_IDENTITY_PACKUMENT_FETCH_FAIL', + `The packument endpoint (at ${redactUrlCredentials(packumentUrl)}) returned an unexpected body. Expected an object with versions; got: ${JSON.stringify(body)?.slice(0, 500) ?? String(body)}` + ) + } + return body +} + +// Registry URLs may legally embed basic-auth credentials +// (https://user:pass@host/); never print those in error messages, which land +// in terminal output and CI logs. +function redactUrlCredentials (rawUrl: string): string { + try { + const parsed = new url.URL(rawUrl) + parsed.username = '' + parsed.password = '' + return parsed.toString() + } catch { + return rawUrl + } +} + +function redactTextCredentials (text: string): string { + return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^@/\s]+@/gi, '$1') +} + +function verifyPackageSignatures ( + pkg: InstalledPackageToVerify, + publishedAt: string | undefined, + signatures: PackageSignature[], + keys: RegistryKey[] +): { reason: string, category: SignatureFailureCategory } | undefined { + // Registry signatures cover the package identity and content integrity. + const message = `${pkg.name}@${pkg.version}:${pkg.integrity}` + const publishedTime = publishedAt ? Date.parse(publishedAt) : undefined + + // A package is accepted as soon as ONE signature made by a trusted key + // validates. Signatures from unknown/expired/invalid keys are recorded but do + // not on their own fail the package — otherwise a key rotation (a packument + // carrying multiple signatures) breaks, and a mirror could force a failure + // just by appending a junk signature. We fail only when no signature validates + // against a trusted key. + const failures: string[] = [] + for (const signature of signatures) { + const key = keys.find(({ keyid }) => keyid === signature.keyid) + if (!key) { + failures.push(`${pkg.name}@${pkg.version} has a registry signature with keyid ${signature.keyid} but no corresponding public key can be found`) + continue + } + // Key expiry is a consistency check, not a security boundary: the publish + // time comes from the same unauthenticated packument as the signatures, so + // a forger holding an expired trusted key could backdate it anyway. The + // signature verification below is what gates acceptance. + if (key.expires && publishedTime != null && publishedTime >= Date.parse(key.expires)) { + failures.push(`${pkg.name}@${pkg.version} has a registry signature with keyid ${signature.keyid} but the corresponding public key has expired ${key.expires}`) + continue + } + const pem = `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----` + // crypto.verify can throw on malformed PEM key material or signature bytes + // returned by the registry; treat any failure as an invalid signature so + // one bad key doesn't crash the whole verification. + let verified: boolean + try { + const verifier = crypto.createVerify('SHA256') + verifier.write(message) + verifier.end() + verified = verifier.verify(pem, signature.sig, 'base64') + } catch { + verified = false + } + if (verified) return undefined + failures.push(`${pkg.name}@${pkg.version} has an invalid registry signature with keyid ${signature.keyid}`) + } + // Prefer an invalid signature from a known key (a tamper signal) over an + // unknown-key or expiry reason, since unknown keys may just be junk a mirror + // appended. + const reason = failures.find((failure) => failure.includes('invalid registry signature')) ?? + failures[0] ?? + `${pkg.name}@${pkg.version} has no registry signature from a trusted key` + return { reason, category: 'invalid' } +} + +function toUri (pkgName: string, registry: string): string { + let encodedName: string + if (pkgName[0] === '@') { + encodedName = `@${encodeURIComponent(pkgName.slice(1))}` + } else { + encodedName = encodeURIComponent(pkgName) + } + return new url.URL(encodedName, registry.endsWith('/') ? registry : `${registry}/`).toString() +} + +function isPackument (body: unknown): body is Packument { + return typeof body === 'object' && body != null && typeof (body as Packument).versions === 'object' && (body as Packument).versions != null +} + +function isPackageSignature (signature: unknown): signature is PackageSignature { + return typeof signature === 'object' && signature != null && + typeof (signature as PackageSignature).keyid === 'string' && + typeof (signature as PackageSignature).sig === 'string' +} diff --git a/tools/plugin-commands-self-updater/test/selfUpdate.test.ts b/tools/plugin-commands-self-updater/test/selfUpdate.test.ts index eee3f967df..5e03ac9cf7 100644 --- a/tools/plugin-commands-self-updater/test/selfUpdate.test.ts +++ b/tools/plugin-commands-self-updater/test/selfUpdate.test.ts @@ -61,6 +61,9 @@ function prepareOptions (dir: string) { virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120, dir, managePackageManagerVersions: false, + // The fixture pnpm installed here is not signed with npm's real keys, so + // skip the engine identity signature check (empty trusted-keys = skip). + trustedKeys: [], } } diff --git a/tools/plugin-commands-self-updater/test/verifyPnpmEngineIdentity.test.ts b/tools/plugin-commands-self-updater/test/verifyPnpmEngineIdentity.test.ts new file mode 100644 index 0000000000..e91ddbf022 --- /dev/null +++ b/tools/plugin-commands-self-updater/test/verifyPnpmEngineIdentity.test.ts @@ -0,0 +1,227 @@ +import crypto from 'crypto' +import fs from 'fs' +import path from 'path' + +import { tempDir } from '@pnpm/prepare' +import nock from 'nock' + +import { verifyPnpmEngineIdentity, type RegistryKey } from '@pnpm/tools.plugin-commands-self-updater' + +const REGISTRY = 'https://registry.example.test/' +const PNPM_INTEGRITY = 'sha512-pnpm-integrity' +const EXE_INTEGRITY = 'sha512-exe-integrity' +const PLATFORM_INTEGRITY = 'sha512-platform-integrity' +const PLATFORM_PKG_NAME = '@pnpm/test-platform-arch' + +beforeEach(() => { + nock.cleanAll() + nock.disableNetConnect() +}) + +afterEach(() => { + nock.enableNetConnect() +}) + +test('resolves when pnpm carries a valid registry signature over the installed bytes', async () => { + const key = createSigningKey() + mockPackument('pnpm', PNPM_INTEGRITY, [{ keyid: key.keyid, sig: key.sign('pnpm@9.1.0', PNPM_INTEGRITY) }]) + const stage = stageWithPnpmLockfile() + + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', optsTrusting(key))).resolves.toBeUndefined() +}) + +test('throws when the installed bytes do not match what the registry signed (tamper)', async () => { + const key = createSigningKey() + // The registry signed the genuine integrity, but the staged lockfile pins a different one. + mockPackument('pnpm', PNPM_INTEGRITY, [{ keyid: key.keyid, sig: key.sign('pnpm@9.1.0', 'sha512-genuine-pnpm') }]) + const stage = stageWithPnpmLockfile() + + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', optsTrusting(key))).rejects.toThrow(/Refusing to run pnpm/) +}) + +test('throws when the engine is signed by a key pnpm does not trust', async () => { + const signingKey = createSigningKey() + mockPackument('pnpm', PNPM_INTEGRITY, [{ keyid: signingKey.keyid, sig: signingKey.sign('pnpm@9.1.0', PNPM_INTEGRITY) }]) + const stage = stageWithPnpmLockfile() + + // Trust a different key than the one that signed. + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', optsTrusting(createSigningKey()))).rejects.toThrow(/Refusing to run pnpm/) +}) + +test('throws when the engine version is absent from the registry', async () => { + nock(REGISTRY).get('/pnpm').reply(404, {}) + const stage = stageWithPnpmLockfile() + + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', optsTrusting(createSigningKey()))).rejects.toThrow(/Refusing to run pnpm/) +}) + +test('throws (fails closed) when the registry is unreachable', async () => { + // No intercept registered and net connect disabled, so the packument fetch fails. + const stage = stageWithPnpmLockfile() + + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', optsTrusting(createSigningKey()))).rejects.toThrow(/Refusing to run pnpm/) +}) + +test('does not leak registry credentials embedded in the registry URL into error messages', async () => { + const stage = stageWithPnpmLockfile() + const opts = { + ...optsTrusting(createSigningKey()), + registries: { default: 'https://user:hunter2@registry.example.test/' }, + } + + // A non-200 response embeds the packument URL in the error. + nock('https://registry.example.test/').get('/pnpm').reply(500, 'server error').persist() + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', opts)).rejects.toThrow(/Refusing to run pnpm/) + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', opts)).rejects.not.toThrow(/hunter2/) + + // An unreachable registry surfaces the fetch-layer error, which embeds the + // request URL. + nock.cleanAll() + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', opts)).rejects.toThrow(/Refusing to run pnpm/) + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', opts)).rejects.not.toThrow(/hunter2/) +}) + +test('skips (no throw) when no trusted keys are provided', async () => { + const stage = tempDir(false) + + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', { ...baseOpts(), trustedKeys: [] })).resolves.toBeUndefined() +}) + +test('throws when the engine component in the staged lockfile has no integrity metadata', async () => { + const key = createSigningKey() + mockPackument('pnpm', PNPM_INTEGRITY, [{ keyid: key.keyid, sig: key.sign('pnpm@9.1.0', PNPM_INTEGRITY) }]) + const stage = tempDir(false) + writeStageLockfile(stage, [ + 'lockfileVersion: \'9.0\'', + 'importers:', + ' .:', + ' dependencies:', + ' pnpm:', + ' specifier: 9.1.0', + ' version: 9.1.0', + 'packages:', + ' pnpm@9.1.0:', + ` resolution: {tarball: ${REGISTRY}pnpm/-/pnpm-9.1.0.tgz}`, + 'snapshots:', + ' pnpm@9.1.0: {}', + ]) + + await expect(verifyPnpmEngineIdentity(stage, 'pnpm', '9.1.0', optsTrusting(key))).rejects.toThrow(/integrity metadata is missing/) +}) + +test('verifies the platform binary materialized for an @pnpm/exe install', async () => { + const key = createSigningKey() + mockPackument('@pnpm/exe', EXE_INTEGRITY, [{ keyid: key.keyid, sig: key.sign('@pnpm/exe@9.1.0', EXE_INTEGRITY) }]) + mockPackument(PLATFORM_PKG_NAME, PLATFORM_INTEGRITY, [{ keyid: key.keyid, sig: key.sign(`${PLATFORM_PKG_NAME}@9.1.0`, PLATFORM_INTEGRITY) }]) + const stage = stageWithExeLockfile(PLATFORM_INTEGRITY) + + await expect(verifyPnpmEngineIdentity(stage, '@pnpm/exe', '9.1.0', optsTrusting(key))).resolves.toBeUndefined() +}) + +test('throws when the platform binary signature does not validate over the installed bytes', async () => { + const key = createSigningKey() + mockPackument('@pnpm/exe', EXE_INTEGRITY, [{ keyid: key.keyid, sig: key.sign('@pnpm/exe@9.1.0', EXE_INTEGRITY) }]) + // The registry signed a different (genuine) platform binary than the one staged. + mockPackument(PLATFORM_PKG_NAME, PLATFORM_INTEGRITY, [{ keyid: key.keyid, sig: key.sign(`${PLATFORM_PKG_NAME}@9.1.0`, 'sha512-genuine-platform') }]) + const stage = stageWithExeLockfile(PLATFORM_INTEGRITY) + + await expect(verifyPnpmEngineIdentity(stage, '@pnpm/exe', '9.1.0', optsTrusting(key))).rejects.toThrow(/Refusing to run pnpm/) +}) + +function baseOpts () { + return { + rawConfig: {}, + registries: { default: REGISTRY }, + retry: { retries: 0 }, + } +} + +function optsTrusting (key: ReturnType) { + const trustedKey: RegistryKey = { + expires: null, + key: key.publicKey, + keyid: key.keyid, + keytype: 'ecdsa-sha2-nistp256', + scheme: 'ecdsa-sha2-nistp256', + } + return { ...baseOpts(), trustedKeys: [trustedKey] } +} + +function stageWithPnpmLockfile (): string { + const stage = tempDir(false) + writeStageLockfile(stage, [ + 'lockfileVersion: \'9.0\'', + 'importers:', + ' .:', + ' dependencies:', + ' pnpm:', + ' specifier: 9.1.0', + ' version: 9.1.0', + 'packages:', + ' pnpm@9.1.0:', + ` resolution: {integrity: ${PNPM_INTEGRITY}}`, + 'snapshots:', + ' pnpm@9.1.0: {}', + ]) + return stage +} + +function stageWithExeLockfile (platformIntegrity: string): string { + const stage = tempDir(false) + writeStageLockfile(stage, [ + 'lockfileVersion: \'9.0\'', + 'importers:', + ' .:', + ' dependencies:', + ' \'@pnpm/exe\':', + ' specifier: 9.1.0', + ' version: 9.1.0', + 'packages:', + ' \'@pnpm/exe@9.1.0\':', + ` resolution: {integrity: ${EXE_INTEGRITY}}`, + ` '${PLATFORM_PKG_NAME}@9.1.0':`, + ` resolution: {integrity: ${platformIntegrity}}`, + 'snapshots:', + ' \'@pnpm/exe@9.1.0\':', + ' optionalDependencies:', + ` '${PLATFORM_PKG_NAME}': 9.1.0`, + ` '${PLATFORM_PKG_NAME}@9.1.0':`, + ' optional: true', + ]) + // Only platform packages actually materialized on disk are verified. + fs.mkdirSync(path.join(stage, 'node_modules', PLATFORM_PKG_NAME), { recursive: true }) + return stage +} + +function writeStageLockfile (stage: string, lines: string[]): void { + fs.writeFileSync(path.join(stage, 'pnpm-lock.yaml'), `${lines.join('\n')}\n`, 'utf8') +} + +function mockPackument (name: string, integrity: string, signatures: unknown): void { + const encodedPath = name[0] === '@' ? `/@${encodeURIComponent(name.slice(1))}` : `/${name}` + nock(REGISTRY) + .get(encodedPath) + .reply(200, { + name, + time: { '9.1.0': '2024-01-01T00:00:00.000Z' }, + versions: { + '9.1.0': { name, version: '9.1.0', dist: { integrity, signatures, tarball: `${REGISTRY}${name}/-/x-9.1.0.tgz` } }, + }, + }) + .persist() +} + +function createSigningKey (): { keyid: string, publicKey: string, sign: (id: string, integrity: string) => string } { + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' }) + const publicKeyPem = publicKey.export({ format: 'pem', type: 'spki' }).toString() + return { + keyid: `SHA256:test-key-${crypto.randomBytes(4).toString('hex')}`, + publicKey: publicKeyPem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, ''), + sign: (id, integrity) => { + const signer = crypto.createSign('SHA256') + signer.write(`${id}:${integrity}`) + signer.end() + return signer.sign(privateKey, 'base64') + }, + } +} diff --git a/tools/plugin-commands-self-updater/tsconfig.json b/tools/plugin-commands-self-updater/tsconfig.json index d7e5a9891e..695cad66ec 100644 --- a/tools/plugin-commands-self-updater/tsconfig.json +++ b/tools/plugin-commands-self-updater/tsconfig.json @@ -21,15 +21,33 @@ { "path": "../../config/config" }, + { + "path": "../../config/pick-registry-for-package" + }, { "path": "../../env/path" }, { "path": "../../exec/pnpm-cli-runner" }, + { + "path": "../../lockfile/fs" + }, + { + "path": "../../lockfile/types" + }, + { + "path": "../../network/auth-header" + }, + { + "path": "../../network/fetch" + }, { "path": "../../packages/error" }, + { + "path": "../../packages/types" + }, { "path": "../../pkg-manager/client" },