Files
pnpm/cspell.json
Zoltan Kochan 3d50680eda fix(security): verify Node.js runtime SHASUMS OpenPGP signature (#12295)
Follow-up to #12292 (which verifies the **package-manager** binary). This closes the same class of gap for the **Node.js runtime**.

When a repository requests a Node.js runtime — `devEngines.runtime: node@X` (with `onFail: download`, the default) or `useNodeVersion` — pnpm downloads and then executes a Node binary (it's used to run lifecycle / `run` / `exec` scripts). The download **mirror is repository-configurable** via `node-mirror:<channel>` (`nodeDownloadMirrors`) in project `.npmrc`, and the integrity comes from `SHASUMS256.txt` fetched **from that same mirror**.

That's a circular check: a malicious mirror serves a tampered `node` tarball **and** a matching `SHASUMS256.txt`, the sha256 check passes, and pnpm runs the binary. Drive-by on a normal command in a cloned repo.

## Fix

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. A mirror that serves a tampered binary cannot also produce a valid signature, so verification fails. Any faithful mirror (one that proxies the real signed SHASUMS) keeps working.

- `@pnpm/crypto.shasums-file`: new `fetchVerifiedNodeShasums` / `fetchVerifiedNodeShasumsFile` verify the signature via `openpgp` against the embedded keys.
- The keys live in a generated file (`src/nodeReleaseKeys.ts`, 28 keys) mirrored from the canonical `nodejs/release-keys` list. `crypto/shasums-file/scripts/update-node-release-keys.mjs` keeps them current (`pnpm check:node-release-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate so a new release signer can't silently break verification.
- `@pnpm/engine.runtime.node-resolver` verifies the **configurable-mirror** SHASUMS. The hardcoded `unofficial-builds.nodejs.org` musl mirror is **not** repo-configurable and is signed by a different key, so it stays trusted over TLS.

## Scope

- **Pre-release channels (rc, nightly, …) are not verified** — Node only signs the `release` channel (no `SHASUMS256.txt.sig` exists for them, even on nodejs.org), so they remain unverifiable. Verification is gated on the `release` channel.
- **Bun / Deno are unaffected** — their download/SHASUMS URLs are hardcoded to canonical GitHub (`github.com/oven-sh/bun`, `api.github.com/repos/denoland/deno`), not mirror-configurable, so a repo can't redirect them.
- **Pacquet parity:** `pacquet/crates/engine-runtime-node-resolver` has the same mirror-configurable SHASUMS logic and needs the equivalent Rust port — tracked as a follow-up (per the repo's parity rule, opening the TS side first).
2026-06-10 00:33:31 +02:00

412 lines
6.3 KiB
JSON

{
"ignorePaths": [
"**/nodeReleaseKeys.ts",
"**/nodeReleaseKeys.d.ts",
"**/node_release_keys.rs"
],
"words": [
"adduser",
"adipiscing",
"agentkeepalive",
"agentkeepalive's",
"amet",
"andreineculau",
"appdata",
"applyq",
"archy",
"argumentless",
"armv",
"autocheckpoint",
"autocompleting",
"autofix",
"autofixed",
"autoinstalled",
"autozoom",
"babek",
"Backblaze",
"badheaders",
"baires",
"behaviour",
"blabla",
"Bluesky",
"brasileiro",
"bryntum",
"buildx",
"cafile",
"cafs",
"camelcase",
"canonicalizer",
"cantopen",
"canva",
"cerbos",
"certfile",
"clonedeep",
"cmds",
"Codeberg",
"codeload",
"codenames",
"codesign",
"colorterm",
"comver",
"copyfiles",
"corejs",
"corepack",
"corge",
"cowsay",
"Creds",
"cryptiles",
"cves",
"cwsay",
"cyclonedx",
"deburr",
"dedup",
"denoland",
"denolib",
"deptype",
"desugared",
"desugars",
"devextreme",
"devowl",
"dgimuvys",
"didyoumean",
"dirtyforms",
"diskusage",
"dislink",
"dpkg",
"drivelist",
"duplexify",
"eagain",
"ebadplatform",
"ebusy",
"eexist",
"ehrkoext",
"eintegrity",
"eisdir",
"elifecycle",
"elit",
"embedder",
"emfile",
"enametoolong",
"endregion",
"eneedauth",
"enoent",
"enotempty",
"enten",
"eotp",
"eperm",
"epipe",
"errcode",
"esac",
"etamponi",
"exdev",
"execa",
"exploitability",
"fakehash",
"fellback",
"fetchings",
"filenamify",
"filesystem",
"filesystems",
"fnumber",
"foobarqar",
"foofoo",
"footgun",
"forgejo",
"fsevents",
"gabor",
"garply",
"gcttmf",
"getattr",
"ghes",
"ghsa",
"ghsas",
"gitea",
"globalconfig",
"globstar",
"gpgsign",
"grault",
"gruntfile",
"gwhitney",
"haptics",
"hardlink",
"hardlinked",
"hardlinking",
"hardlinks",
"hashbang",
"highmaps",
"hikljmi",
"hoistable",
"homepath",
"hosters",
"htpasswd",
"hyperdrive",
"idempotency",
"imagetools",
"imurmurhash",
"invalidformat",
"ionicons",
"isexe",
"istvan",
"italiano",
"jega",
"jhcg",
"jnbpamcxayl",
"junyi",
"kebabcase",
"kevva",
"keyfile",
"keyid",
"keytype",
"killcb",
"kochan",
"koorchik",
"ldid",
"ldni",
"leniolabs",
"libc",
"libnpmpublish",
"libnpx",
"libsql",
"libzip",
"licence",
"licences",
"lifecycles",
"linuxstatic",
"localappdata",
"lockfiles",
"loglevel",
"logstream",
"longlink",
"longpaths",
"luca",
"martensson",
"maxtimeout",
"mdast",
"metafile",
"millis",
"minioadmin",
"mintimeout",
"mmap",
"monorepolint",
"moonrepo",
"mountpoint",
"msgpack",
"msgpackr",
"msvc",
"msys",
"mycomp",
"mycompany",
"myorg",
"mypackage",
"mytoken",
"ndjson",
"nerfed",
"newversion",
"nistp",
"NOASSERTION",
"nodetouch",
"noent",
"nonexec",
"noninjected",
"nonvulnerable",
"nopadding",
"noproxy",
"nosystem",
"nothrow",
"npmcli",
"npmignore",
"npmjs",
"npmx",
"ntfs",
"nushell",
"ofjergrg",
"onclickoutside",
"oomol",
"openpgp",
"ossl",
"outfile",
"overrider",
"packlist",
"packr",
"packument",
"packuments",
"pacquet",
"paralleljs",
"parallelly",
"parseable",
"partialmatch",
"pathext",
"pegjs",
"pidtree",
"pify",
"pkgname",
"pkgs",
"plotly",
"plugh",
"pnpmfile",
"pnpmfiles",
"pnpmjs",
"pnpmrc",
"pnpmtest",
"pnpr",
"polyfilling",
"português",
"posix",
"postbuild",
"postfoo",
"postpack",
"postprepare",
"postpublish",
"postrestart",
"postshrinkwrap",
"poststart",
"poststop",
"posttest",
"postuninstall",
"postversion",
"preact",
"prebuild",
"prefoo",
"prefs",
"preinstall",
"premajor",
"preminor",
"prepatch",
"prepublish",
"prereleases",
"prerestart",
"preshrinkwrap",
"prestart",
"prestop",
"preuninstall",
"preversion",
"prioritizer",
"promisified",
"proxied",
"pwsh",
"qrcode",
"quux",
"rcompare",
"redownload",
"refclone",
"refetched",
"reflattened",
"reflink",
"reflinked",
"reflinks",
"rehoist",
"reimagining",
"reka",
"Rekor",
"relinks",
"renderable",
"replit",
"reqheaders",
"rescopable",
"rescope",
"rescoped",
"rescopes",
"rescoping",
"rimrafed",
"rmgr",
"rpmdevtools",
"rpmlint",
"rstacruz",
"rushstack",
"rustup",
"safecrlf",
"scopeless",
"sdiff",
"searchexclude",
"searchlimit",
"searchopts",
"searchstaleness",
"sels",
"semistrict",
"serp",
"serverjs",
"shasums",
"sheetjs",
"shlex",
"sigstore",
"sindresorhus",
"sirv",
"SLSA",
"soporan",
"sopts",
"spdxdocs",
"SPDXID",
"sqld",
"srcset",
"ssri",
"stackblitz",
"stacktracey",
"stdtype",
"streamsearch",
"stringifying",
"subcmd",
"subdep",
"subdependencies",
"subdependency",
"subdeps",
"subdir",
"subdirs",
"subkey",
"subkeys",
"subpkg",
"subresource",
"supercede",
"syml",
"syncer",
"syscall",
"syscalls",
"szia",
"tabtab",
"taffydb",
"tarballtemplate",
"teambit",
"tempy",
"testcase",
"tlog",
"TLSV",
"toctou",
"todomvc",
"toplevel",
"TOTP",
"tsgo",
"tsparticles",
"turso",
"typecheck",
"unallowed",
"undeprecate",
"underperformance",
"undollar",
"unextractable",
"uninstallation",
"unnest",
"unparseable",
"unreviewed",
"unskip",
"unstar",
"usecase",
"userconfig",
"userprofile",
"ustar",
"uuidv",
"valign",
"vuln",
"webauth",
"webcontainer",
"winst",
"workleap",
"worktree",
"worktrees",
"wrappy",
"xmarw",
"yazl",
"zkochan",
"zoli",
"zoltan"
]
}