Skip to content

fix(security): verify Node.js runtime SHASUMS OpenPGP signature#12295

Merged
zkochan merged 9 commits into
mainfrom
verify-node-runtime-shasums
Jun 9, 2026
Merged

fix(security): verify Node.js runtime SHASUMS OpenPGP signature#12295
zkochan merged 9 commits into
mainfrom
verify-node-runtime-shasums

Conversation

@zkochan

@zkochan zkochan commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

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).

Testing

  • @pnpm/crypto.shasums-file: 7 (incl. 4 new — valid signature, untrusted key, tampered content, missing .sig), using openpgp-generated test keys.
  • @pnpm/engine.runtime.node-resolver: 54 passing.
  • Verified against live nodejs.org: the embedded keys verify a real v22.11.0 SHASUMS256.txt signature end-to-end.
  • Keys freshness script verified against the canonical list; build + lint + cspell + meta-updater clean.

Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • New Features

    • Node.js SHASUMS downloads are now verified via detached OpenPGP signatures against pinned Node.js release keys; verification applies to the official release channel only (musl/unofficial builds remain trusted without signature verification).
  • Release Process

    • Release creation now validates embedded Node.js release keys against the canonical list and fails if they drift.
  • Tests

    • Added tests covering signature verification success and multiple failure modes.
  • Chores

    • Added key-management scripts and updated crypto-related dependencies.
  • Documentation

    • Security note clarifying verification behavior and trusted sources.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds detached OpenPGP signature verification of Node.js SHASUMS256.txt using pinned Node release-team keys, a key-sync CLI and release-time check, JS and Rust verified fetch APIs with tests, and resolver wiring that enables verification for official releases only.

Changes

Node.js SHASUMS OpenPGP Verification

Layer / File(s) Summary
Dependencies and Infrastructure Setup
pnpm-workspace.yaml, crypto/shasums-file/package.json, package.json, cspell.json, Cargo.toml
Catalog pins for openpgp and @openpgp/web-stream-tools, workspace Rust pgp dependency, package scripts added for key management, cspell configured to ignore generated key files, and workspace overrides.
Node Release Key Synchronization Tool
crypto/shasums-file/scripts/update-node-release-keys.mjs, package.json, .github/workflows/create-release-pr.yml
CLI that validates canonical nodejs/release-keys fingerprints and can fetch/regenerate NODE_RELEASE_KEYS entries in both the TypeScript and Rust sources (--update); the release workflow runs the check during release PR creation.
OpenPGP Signature Verification Implementation (JS)
crypto/shasums-file/src/verifyNodeShasums.ts, crypto/shasums-file/src/index.ts
Adds fetchVerifiedNodeShasums and fetchVerifiedNodeShasumsFile, fetches SHASUMS + detached .sig, parses OpenPGP packets, matches issuer key IDs to pinned keys, verifies signatures, and surfaces fetch/verification errors.
Public API Export and Test Suite (JS)
crypto/shasums-file/src/index.ts, crypto/shasums-file/test/verifyNodeShasums.ts
Re-exports verification API, adds parsing helper to produce ShasumsFileItem[], and a Jest suite covering verification success and multiple failure cases.
Rust verification surface & tests
pacquet/crates/crypto-shasums-file/Cargo.toml, pacquet/crates/crypto-shasums-file/src/lib.rs, pacquet/crates/crypto-shasums-file/src/tests.rs
Adds pgp dependency, implements fetch_verified_node_shasums and fetch_verified_node_shasums_file, introduces FetchVerifiedNodeShasumsError, verification helpers loading embedded node_release_keys, and tests exercising success, tamper, and HTTP failures.
Runtime Resolution Integration (JS & Rust)
engine/runtime/node-resolver/src/index.ts, pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs, pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
Passes release_channel to asset-reading flow; enables signature verification for release channel and disables it for unofficial musl builds; selects verified vs unverified shasums fetchers and maps verification errors; adds tests for release vs prerelease behavior.
Changeset and Release Workflow
.changeset/verify-node-runtime-shasums.md, .github/workflows/create-release-pr.yml, deny.toml, .typos.toml
Documents the security change and adds a workflow step to verify embedded Node release keys against the canonical list during release PR creation; records an advisory ignore and excludes generated key file from typo checks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11783: Earlier work introducing verified shasums fetchers and resolver plumbing that this PR builds upon.

Suggested reviewers

  • KSXGitHub

Poem

🐰
I hopped through bytes and armored keys,
chased a detached sig across the breeze,
matched issuer ids with a careful nod,
wrote keys in TypeScript and stamped in Rust,
crypto rabbit hums — the SHASUMS pleased!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(security): verify Node.js runtime SHASUMS OpenPGP signature' directly and clearly summarizes the main security-focused change across the changeset: adding OpenPGP signature verification for Node.js SHASUMS files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch verify-node-runtime-shasums

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 10.437 ± 0.520 10.042 11.860 1.82 ± 0.12
pacquet@main 10.113 ± 0.072 10.021 10.217 1.76 ± 0.07
pnpr@HEAD 5.978 ± 0.557 5.578 7.001 1.04 ± 0.11
pnpr@main 5.730 ± 0.230 5.536 6.222 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 10.43676948356,
      "stddev": 0.5202391299244099,
      "median": 10.324647991359999,
      "user": 2.41320822,
      "system": 2.5207580199999997,
      "min": 10.04190892886,
      "max": 11.859537288859999,
      "times": [
        10.34311575886,
        11.859537288859999,
        10.51761381986,
        10.22204345086,
        10.04190892886,
        10.30618022386,
        10.362417812859999,
        10.096084506859999,
        10.416974093859999,
        10.20181895086
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 10.113197012959999,
      "stddev": 0.07158809538866599,
      "median": 10.11973201136,
      "user": 2.4190981199999997,
      "system": 2.52458132,
      "min": 10.021184761859999,
      "max": 10.21733413386,
      "times": [
        10.21733413386,
        10.059467870859999,
        10.021340325859999,
        10.12808050386,
        10.158597452859999,
        10.15278313686,
        10.05670814586,
        10.20509027886,
        10.021184761859999,
        10.111383518859999
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.97848290816,
      "stddev": 0.5573971719797916,
      "median": 5.66614571836,
      "user": 2.0164709199999997,
      "system": 2.2590464199999998,
      "min": 5.57829235586,
      "max": 7.00093413886,
      "times": [
        5.6122034568600005,
        5.57931123886,
        5.579665808860001,
        5.57829235586,
        5.69354520986,
        5.63874622686,
        6.85530244386,
        7.00093413886,
        6.3790296598600005,
        5.86779854186
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.73025449086,
      "stddev": 0.2295467073721952,
      "median": 5.61898430986,
      "user": 1.9797764199999999,
      "system": 2.25834952,
      "min": 5.53643263686,
      "max": 6.2223935388600005,
      "times": [
        6.2223935388600005,
        5.59554164686,
        5.57739890886,
        5.56120961586,
        5.543073558860001,
        5.64242697286,
        5.82561998086,
        5.81141013686,
        5.53643263686,
        5.98703791186
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 733.3 ± 73.7 653.1 853.8 1.00
pacquet@main 772.8 ± 185.2 637.9 1217.4 1.05 ± 0.27
pnpr@HEAD 898.9 ± 73.4 782.8 1059.1 1.23 ± 0.16
pnpr@main 836.9 ± 95.5 736.3 957.5 1.14 ± 0.17
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.7332562675600001,
      "stddev": 0.07365974581955163,
      "median": 0.73088119926,
      "user": 0.30286878,
      "system": 1.04679822,
      "min": 0.65311287726,
      "max": 0.8538321802600001,
      "times": [
        0.78658897526,
        0.66279736926,
        0.65767234826,
        0.8538321802600001,
        0.68898976926,
        0.77277262926,
        0.67234340926,
        0.8012297312600001,
        0.78322338626,
        0.65311287726
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.77280382826,
      "stddev": 0.18520812768821024,
      "median": 0.67410889976,
      "user": 0.30664378000000003,
      "system": 1.0407157200000001,
      "min": 0.63787712626,
      "max": 1.21744541226,
      "times": [
        0.77005863726,
        0.90900158726,
        1.21744541226,
        0.8835987012600001,
        0.67878568126,
        0.6492691632600001,
        0.63787712626,
        0.65210233226,
        0.66943211826,
        0.66046752326
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.8988642635599999,
      "stddev": 0.07338307079547653,
      "median": 0.8834099312600001,
      "user": 0.32450838000000004,
      "system": 1.0409758199999999,
      "min": 0.7827774362600001,
      "max": 1.05910579426,
      "times": [
        0.9739014582600001,
        0.87520332326,
        0.8590526942600001,
        0.86723794026,
        0.90447652326,
        1.05910579426,
        0.7827774362600001,
        0.90006760326,
        0.8870121362600001,
        0.87980772626
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.83689378926,
      "stddev": 0.09553138802750825,
      "median": 0.81428109026,
      "user": 0.31204498,
      "system": 1.0480999199999999,
      "min": 0.73627383826,
      "max": 0.95753570026,
      "times": [
        0.76411558026,
        0.73627383826,
        0.95753570026,
        0.94694687526,
        0.76225410726,
        0.94939847726,
        0.74055488226,
        0.89931163926,
        0.86444660026,
        0.74810019226
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.555 ± 0.324 9.280 10.265 1.76 ± 0.08
pacquet@main 9.342 ± 0.076 9.268 9.484 1.72 ± 0.05
pnpr@HEAD 5.431 ± 0.161 5.282 5.753 1.00
pnpr@main 5.464 ± 0.159 5.306 5.759 1.01 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.555473642239999,
      "stddev": 0.3238170416602305,
      "median": 9.427804818039998,
      "user": 2.8526164199999995,
      "system": 2.57155534,
      "min": 9.27974340154,
      "max": 10.264767064539999,
      "times": [
        9.41498570154,
        9.46113716254,
        9.27974340154,
        9.31129472254,
        9.53416141354,
        9.40719416054,
        9.42299701054,
        9.43261262554,
        10.025843159539999,
        10.264767064539999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.34205022224,
      "stddev": 0.07557131025355318,
      "median": 9.31935796454,
      "user": 2.85465472,
      "system": 2.57618204,
      "min": 9.26754756854,
      "max": 9.48377470454,
      "times": [
        9.375277969539999,
        9.34494167254,
        9.26754756854,
        9.48377470454,
        9.315262537539999,
        9.32345339154,
        9.27291886254,
        9.307148180539999,
        9.455995292539999,
        9.27418204254
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.431035551640001,
      "stddev": 0.16069814796728785,
      "median": 5.36104464454,
      "user": 1.8582565199999999,
      "system": 2.1970913399999996,
      "min": 5.28237750454,
      "max": 5.75264986054,
      "times": [
        5.311874025540001,
        5.34882606854,
        5.28237750454,
        5.47913052254,
        5.66446007354,
        5.316086798540001,
        5.459182756540001,
        5.32250468554,
        5.75264986054,
        5.37326322054
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.4644236699399995,
      "stddev": 0.15946235304412754,
      "median": 5.417995874040001,
      "user": 1.8621950199999997,
      "system": 2.19074094,
      "min": 5.30558970454,
      "max": 5.758867271540001,
      "times": [
        5.32089376454,
        5.457936359540001,
        5.30558970454,
        5.32453622354,
        5.35121689354,
        5.627292096540001,
        5.483519971540001,
        5.63632902554,
        5.758867271540001,
        5.37805538854
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.461 ± 0.307 1.296 2.257 2.07 ± 0.51
pacquet@main 1.392 ± 0.151 1.270 1.627 1.97 ± 0.33
pnpr@HEAD 0.707 ± 0.092 0.668 0.969 1.00
pnpr@main 0.707 ± 0.099 0.661 0.985 1.00 ± 0.19
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.4612504321200002,
      "stddev": 0.3067133042021927,
      "median": 1.32465142312,
      "user": 1.23569606,
      "system": 1.4514640599999997,
      "min": 1.29597593162,
      "max": 2.2567697216200004,
      "times": [
        1.7128889496200002,
        1.35808529862,
        1.30374868762,
        2.2567697216200004,
        1.31789400362,
        1.29654664962,
        1.29597593162,
        1.31311434262,
        1.42607189362,
        1.33140884262
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3917729469200002,
      "stddev": 0.15104652396537913,
      "median": 1.30043985912,
      "user": 1.2587557600000001,
      "system": 1.4184824599999997,
      "min": 1.26967017162,
      "max": 1.62709946362,
      "times": [
        1.36966458562,
        1.26967017162,
        1.62709946362,
        1.5958643856199999,
        1.59736432862,
        1.30704227862,
        1.29383743962,
        1.29210627062,
        1.27661351862,
        1.28846702662
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.70729447032,
      "stddev": 0.09225228821408565,
      "median": 0.67949588762,
      "user": 0.26980735999999994,
      "system": 1.0048969599999997,
      "min": 0.66778266962,
      "max": 0.9693882086200001,
      "times": [
        0.9693882086200001,
        0.68867512162,
        0.67781064962,
        0.67951945962,
        0.67947231562,
        0.68034325962,
        0.67555849262,
        0.66778266962,
        0.67311504962,
        0.68127947662
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7074020961199999,
      "stddev": 0.09856031558983139,
      "median": 0.67318472512,
      "user": 0.26955535999999997,
      "system": 1.0027182599999998,
      "min": 0.6606533946200001,
      "max": 0.98495697762,
      "times": [
        0.66891284362,
        0.67445907862,
        0.66957648162,
        0.67026575862,
        0.6606533946200001,
        0.67191037162,
        0.98495697762,
        0.71327862762,
        0.68406452962,
        0.67594289762
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.920 ± 0.099 4.837 5.167 7.31 ± 0.19
pacquet@main 4.928 ± 0.054 4.845 5.029 7.32 ± 0.14
pnpr@HEAD 0.673 ± 0.011 0.660 0.697 1.00
pnpr@main 0.724 ± 0.100 0.667 0.988 1.08 ± 0.15
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.9198433403,
      "stddev": 0.0988643991716069,
      "median": 4.9004285382,
      "user": 1.3849202600000001,
      "system": 1.54288412,
      "min": 4.8368935587,
      "max": 5.1668876857,
      "times": [
        4.8368935587,
        4.8981761267,
        4.8389979687,
        4.8633124757,
        5.1668876857,
        4.9899978507,
        4.9286494537,
        4.9026809497,
        4.9205258567,
        4.8523114767
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.927556420899999,
      "stddev": 0.053885751866278545,
      "median": 4.9279674772,
      "user": 1.39122616,
      "system": 1.5397104199999998,
      "min": 4.8451862247,
      "max": 5.0291285767,
      "times": [
        4.9761666627,
        4.8550545567,
        4.9450131487,
        4.9283010777000005,
        4.9276338767,
        5.0291285767,
        4.9268040287,
        4.9437608577,
        4.8451862247,
        4.8985151987
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6732972241,
      "stddev": 0.010750405051886227,
      "median": 0.6720544977,
      "user": 0.26290956000000004,
      "system": 1.01383192,
      "min": 0.6595788457,
      "max": 0.6974073577000001,
      "times": [
        0.6709648307000001,
        0.6623596807000001,
        0.6740547877,
        0.6731441647,
        0.6974073577000001,
        0.6790019037000001,
        0.6702493757000001,
        0.6802846107000001,
        0.6659266837000001,
        0.6595788457
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7244257625,
      "stddev": 0.10030965678369738,
      "median": 0.6811041922000001,
      "user": 0.26755586,
      "system": 1.01959122,
      "min": 0.6673374847000001,
      "max": 0.9877694127000001,
      "times": [
        0.7126153867000001,
        0.6673374847000001,
        0.6827851457,
        0.6783511467000001,
        0.6794232387000001,
        0.9877694127000001,
        0.7981824147000001,
        0.6949498567000001,
        0.6712590697,
        0.6715844687000001
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12295
Testbedpacquet

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
9.56 s
(+33.47%)Baseline: 7.16 s
8.59 s
(111.22%)

Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
9,555.47 ms
(+33.47%)Baseline: 7,159.34 ms
8,591.21 ms
(111.22%)

isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
4,919.84 ms
(-1.96%)Baseline: 5,018.19 ms
6,021.83 ms
(81.70%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,461.25 ms
(+3.66%)Baseline: 1,409.71 ms
1,691.65 ms
(86.38%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
10,436.77 ms
(+14.12%)Baseline: 9,145.30 ms
10,974.36 ms
(95.10%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
733.26 ms
(+12.63%)Baseline: 651.03 ms
781.23 ms
(93.86%)
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan marked this pull request as ready for review June 9, 2026 20:27
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0)

Grey Divider


Action required

1. Unused KeyDetails import 🐞 Bug ⚙ Maintainability
Description
pacquet/crates/crypto-shasums-file/src/lib.rs imports pgp::types::KeyDetails but never uses it,
triggering the unused_imports lint. Pacquet CI runs cargo clippy ... -D warnings, so this will
fail the workflow and block merges.
Code

pacquet/crates/crypto-shasums-file/src/lib.rs[R31-34]

+use pgp::{
+    composed::{Deserializable, DetachedSignature, SignedPublicKey},
+    types::KeyDetails,
+};
Evidence
The file contains an unused KeyDetails import, and the repository’s pacquet CI explicitly treats
warnings as errors during clippy, which makes unused_imports merge-blocking.

pacquet/crates/crypto-shasums-file/src/lib.rs[31-34]
.github/workflows/pacquet-ci.yml[97-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`KeyDetails` is imported from `pgp::types` but not used anywhere in the module. Because CI runs clippy with `-D warnings`, this unused import will be treated as an error.
## Issue Context
This was introduced with the new OpenPGP verification logic.
## Fix Focus Areas
- pacquet/crates/crypto-shasums-file/src/lib.rs[31-34]
## Suggested fix
Remove `types::KeyDetails` from the `use pgp::{ ... }` import list (or start using it if it was intended, but there is no current usage).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Revoked keys still trusted ✓ Resolved 🐞 Bug ⛨ Security
Description
update-node-release-keys.mjs only fails when canonical fingerprints are missing from the embedded
list, but does not fail when embedded keys are no longer present in the canonical
nodejs/release-keys list. This can leave removed/revoked Node release keys trusted indefinitely,
undermining the trust anchor used by fetchVerifiedNodeShasums.
Code

crypto/shasums-file/scripts/update-node-release-keys.mjs[R24-35]

+  const embedded = readEmbeddedFingerprints()
+  const missing = fingerprints.filter((fp) => !embedded.includes(fp))
+
+  if (!update) {
+    if (missing.length === 0) {
+      console.log(`✓ Embedded Node.js release keys are up to date (${embedded.length} key(s)).`)
+      return
+    }
+    console.error(`✗ Embedded Node.js release keys are out of date. Missing: ${missing.join(', ')}`)
+    console.error(`Run: node ${path.relative(process.cwd(), fileURLToPath(import.meta.url))} --update`)
+    process.exit(1)
+  }
Evidence
The script’s check mode only computes missing and returns success when missing.length === 0, so
it never detects keys that exist in the embedded file but were removed upstream. Separately,
fetchVerifiedNodeShasums defaults to trusting the embedded NODE_RELEASE_KEYS, so stale embedded
keys remain part of the runtime trust set.

crypto/shasums-file/scripts/update-node-release-keys.mjs[19-35]
crypto/shasums-file/src/verifyNodeShasums.ts[44-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`crypto/shasums-file/scripts/update-node-release-keys.mjs` only checks for *missing* upstream fingerprints and reports success if none are missing. If `nodejs/release-keys` removes a fingerprint (e.g., key revocation/rotation), pnpm’s embedded `NODE_RELEASE_KEYS` can still contain it and the release gate will still pass, leaving a stale key trusted.
### Issue Context
The embedded key set is used as the trust anchor for verifying Node’s `SHASUMS256.txt.sig` (via `fetchVerifiedNodeShasums`). Keeping revoked/removed keys trusted weakens the security guarantee of the new signature verification.
### Fix
In check mode, compute both:
- `missing = canonical - embedded`
- `extra = embedded - canonical`
Fail the check if either is non-empty, and print both lists (when present) with the existing “Run --update” hint. Update mode can remain unchanged (it already rewrites from the canonical list, which will drop extra keys).
### Fix Focus Areas
- crypto/shasums-file/scripts/update-node-release-keys.mjs[19-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Key parse misreported 🐞 Bug ◔ Observability
Description
In pacquet’s Node SHASUMS verifier, failures while parsing embedded Node.js release public keys are
mapped to SignatureUnreadable, producing the message “Could not read the Node.js SHASUMS
signature” even when the signature is fine. This misclassifies the failure mode and makes
verification incidents harder to diagnose.
Code

pacquet/crates/crypto-shasums-file/src/lib.rs[R265-306]

+fn is_signed_by_trusted_node_release_key(
+    content: &[u8],
+    signature_bytes: &[u8],
+) -> Result<bool, FetchVerifiedNodeShasumsError> {
+    let signature = DetachedSignature::from_bytes(Cursor::new(signature_bytes))
+        .map_err(signature_unreadable)?;
+    for key in trusted_node_release_keys()? {
+        if signature.verify(&key.primary_key, content).is_ok() {
+            return Ok(true);
+        }
+        for subkey in &key.public_subkeys {
+            if signature.verify(subkey, content).is_ok() {
+                return Ok(true);
+            }
+        }
+    }
+    Ok(false)
+}
+
+fn trusted_node_release_keys() -> Result<Vec<SignedPublicKey>, FetchVerifiedNodeShasumsError> {
+    NODE_RELEASE_KEYS.iter().map(read_node_release_key).collect()
+}
+
+fn read_node_release_key(
+    trusted_key: &NodeReleaseKey,
+) -> Result<SignedPublicKey, FetchVerifiedNodeShasumsError> {
+    let (key, _headers) = SignedPublicKey::from_armor_single(trusted_key.armored_key.as_bytes())
+        .map_err(signature_unreadable)?;
+    let actual_fingerprint = key.fingerprint().to_string();
+    let fingerprint_matches = actual_fingerprint.eq_ignore_ascii_case(trusted_key.fingerprint);
+    if !fingerprint_matches {
+        return Err(FetchVerifiedNodeShasumsError::EmbeddedKeyFingerprintMismatch {
+            expected: trusted_key.fingerprint,
+            actual: actual_fingerprint,
+        });
+    }
+    Ok(key)
+}
+
+fn signature_unreadable(error: pgp::errors::Error) -> FetchVerifiedNodeShasumsError {
+    FetchVerifiedNodeShasumsError::SignatureUnreadable { error: Arc::new(error) }
+}
Evidence
The error message for SignatureUnreadable is signature-specific, but the same mapping helper
(signature_unreadable) is used for both signature parsing and embedded key parsing, so a key-parse
failure will emit a misleading signature-parse error.

pacquet/crates/crypto-shasums-file/src/lib.rs[89-105]
pacquet/crates/crypto-shasums-file/src/lib.rs[265-306]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FetchVerifiedNodeShasumsError::SignatureUnreadable` is used for both (a) parsing the detached signature bytes and (b) parsing the embedded public keys. If an embedded key is malformed, the error message incorrectly claims the SHASUMS *signature* couldn’t be read.
### Issue Context
This is surfaced via the new Node.js SHASUMS signature verification path in `pacquet_crypto_shasums_file` and will be wrapped by `NodeResolverError::FetchVerifiedNodeShasums`.
### Fix Focus Areas
- pacquet/crates/crypto-shasums-file/src/lib.rs[89-105]
- pacquet/crates/crypto-shasums-file/src/lib.rs[265-306]
### Suggested fix
- Introduce a distinct error variant for embedded key parsing (e.g. `EmbeddedKeyUnreadable { error }`) with a message like `Could not read an embedded Node.js release public key: ...`.
- Use that new variant for `SignedPublicKey::from_armor_single(...).map_err(...)`.
- Keep `SignatureUnreadable` for `DetachedSignature::from_bytes(...)` only.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Unbound key fingerprints 🐞 Bug ⛨ Security
Description
The key update script embeds armored keys by expected fingerprint without verifying the key material
actually has that fingerprint, and the runtime verifiers don’t enforce this binding (TS ignores
fingerprints; Rust only debug_asserts). This reduces defense-in-depth and can let drift checks pass
even if embedded fingerprint metadata and key material diverge, potentially breaking verification
for some releases or trusting unintended key material.
Code

crypto/shasums-file/scripts/update-node-release-keys.mjs[R51-55]

+  const keys = []
+  for (const fp of fingerprints) {
+    const armored = (await (await fetchOk(`${RAW}/keys/${fp}.asc`)).text()).trim()
+    keys.push({ fingerprint: fp, armored })
+  }
Evidence
The generator writes fingerprint+armored key pairs without fingerprint validation, TS verification
ignores the fingerprint field entirely when loading trusted keys, and Rust only checks fingerprint
equality via debug_assert which is disabled in release builds.

crypto/shasums-file/scripts/update-node-release-keys.mjs[51-55]
crypto/shasums-file/src/verifyNodeShasums.ts[19-23]
pacquet/crates/crypto-shasums-file/src/lib.rs[265-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`update-node-release-keys.mjs` fetches `keys/<fingerprint>.asc` and writes both the fingerprint string and the fetched armored key into the repo, but it never validates that the armored key’s actual fingerprint matches the expected fingerprint. At runtime, the TS verifier (`verifyNodeShasums.ts`) uses only the armored key material (ignoring the stored fingerprint), and the Rust verifier only `debug_assert!`s fingerprint equality (not enforced in release builds).
This means the repo can end up with a fingerprint/key mismatch that still passes the drift check (which compares only embedded fingerprint strings), and the runtime trust set is effectively “whatever armored keys are embedded”, not “these exact fingerprints”.
## Issue Context
- The embedded key files store both `fingerprint` and `armoredKey`, but the current implementation doesn’t enforce the binding in production.
- This is primarily defense-in-depth / correctness hardening: it prevents silent divergence between expected fingerprints and embedded key material.
## Fix Focus Areas
- crypto/shasums-file/scripts/update-node-release-keys.mjs[51-55]
- crypto/shasums-file/src/verifyNodeShasums.ts[19-23]
- pacquet/crates/crypto-shasums-file/src/lib.rs[265-273]
## Implementation notes
- In `update-node-release-keys.mjs`: after fetching each armored key, parse it (e.g. via `openpgp.readKey`) and compare `key.getFingerprint()` (normalized to uppercase, no spaces) to `fp`; throw if mismatch.
- In TS runtime: when loading `NODE_RELEASE_KEYS`, also validate that each parsed key’s fingerprint matches the embedded `fingerprint` field (only for that bundled key set) and throw a clear `PnpmError` if not.
- In Rust runtime: replace `debug_assert!(fingerprint_matches)` with a real runtime check that returns an error (add an error variant like `KeyFingerprintMismatch { expected, actual }`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. IssuerKeyID required 🐞 Bug ☼ Reliability
Description
signaturePacketVerifies returns false when a signature packet has no issuerKeyID, so any valid
detached signature lacking issuerKeyID will be rejected and Node SHASUMS verification will fail even
if the signature cryptographically matches a trusted key.
Code

crypto/shasums-file/src/verifyNodeShasums.ts[R95-98]

+  const issuerKeyID = signaturePacket.issuerKeyID
+  if (issuerKeyID == null) return false
+  const keyPacket = keyPackets.find((packet) => packet.getKeyID().equals(issuerKeyID))
+  if (keyPacket == null) return false
Evidence
The new verifier currently hard-fails any signature packet without issuerKeyID by returning false
before attempting cryptographic verification, making verification dependent on optional signature
metadata.

crypto/shasums-file/src/verifyNodeShasums.ts[90-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`signaturePacketVerifies()` immediately returns `false` when `signaturePacket.issuerKeyID` is null. This makes signature verification brittle to valid OpenPGP detached signatures that omit issuerKeyID (even if they otherwise verify against a trusted key).
### Issue Context
The verifier already loads all trusted key packets (primary + subkeys) and performs packet-level verification. It can safely fall back to trying verification against all trusted key packets when `issuerKeyID` is absent, without weakening security (verification still requires a cryptographically valid signature).
### Fix Focus Areas
- crypto/shasums-file/src/verifyNodeShasums.ts[90-108]
### Suggested change
- If `issuerKeyID` is present: keep the fast-path lookup by keyID.
- If `issuerKeyID` is missing: attempt `signaturePacket.verify(...)` against **each** trusted key packet until one verifies.
- Keep behavior otherwise identical (still return `false` on verify failures, and only accept when at least one packet verifies).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
6. Unvalidated SHASUMS parsing 🐞 Bug ≡ Correctness
Description
parseShasumsFile builds integrity strings from the first whitespace token without validating it is a
64-hex SHA-256 or that a filename token exists, so malformed SHASUMS content can produce invalid
integrity values and fail later with less actionable errors.
Code

crypto/shasums-file/src/index.ts[R35-38]

+export function parseShasumsFile (shasumsFileContent: string): ShasumsFileItem[] {
const lines = shasumsFileContent.split('\n')
const items: ShasumsFileItem[] = []
for (const line of lines) {
Evidence
parseShasumsFile currently does not validate the SHA token, while the same module’s single-file
lookup path does validate SHA-256 format; downstream integrity parsing rejects invalid formats/empty
digests, so malformed lines will fail later and less clearly.

crypto/shasums-file/src/index.ts[35-47]
crypto/shasums-file/src/index.ts[64-83]
crypto/integrity/src/index.ts[3-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`parseShasumsFile()` accepts any non-empty line, splits it, and converts the first token using `Buffer.from(..., 'hex')` without validating the SHA token length/charset or ensuring `fileName` exists. This can yield invalid or misleading `integrity` values and push errors downstream.
### Issue Context
The same module already has `SHA256_REGEX` validation in `pickFileChecksumFromShasumsFile()`, and downstream integrity checking requires a valid `algo-base64hash` format and a non-empty decoded digest.
### Fix Focus Areas
- crypto/shasums-file/src/index.ts[35-47]
- crypto/shasums-file/src/index.ts[64-87]
### Suggested change
- In `parseShasumsFile`, validate:
- `sha256` matches the existing `SHA256_REGEX`
- `fileName` is a non-empty string
- If a line is malformed, throw a `PnpmError` with a stable code (or skip with an explicit policy, but throwing is safer for security-sensitive SHASUMS).
- Consider reusing `SHA256_REGEX` to keep parsing behavior consistent with `pickFileChecksumFromShasumsFile`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. Misreported key parse errors 🐞 Bug ◔ Observability
Description
In isSignedByTrustedKey(), the Promise.all() try/catch wraps both signature parsing and trusted-key
loading, but the thrown error always claims the SHASUMS signature couldn’t be read. If embedded Node
release keys fail to parse, the resulting incident/error report is misleading and obscures the true
failure mode.
Code

crypto/shasums-file/src/verifyNodeShasums.ts[R72-79]

+  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)}`)
+  }
Evidence
The catch block currently covers both the signature parsing call and the trusted-key loading call,
yet always reports a signature-read failure; key parsing happens inside
loadSigningKeyPackets/readSigningKeyPackets via openpgp.readKey, so key failures are misattributed.

crypto/shasums-file/src/verifyNodeShasums.ts[72-79]
crypto/shasums-file/src/verifyNodeShasums.ts[11-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isSignedByTrustedKey()` wraps both `openpgp.readSignature(...)` and `loadSigningKeyPackets(...)` in a single `try/catch`, but the thrown `PnpmError` message always says it "Could not read the Node.js SHASUMS signature". This is misleading when the failure is actually due to parsing/reading the embedded trusted keys.
### Issue Context
- `loadSigningKeyPackets()` parses trusted armored keys via `openpgp.readKey(...)`, which can throw.
- The current catch block doesn’t distinguish whether the error came from signature parsing or key parsing.
### Fix Focus Areas
- crypto/shasums-file/src/verifyNodeShasums.ts[11-23]
- crypto/shasums-file/src/verifyNodeShasums.ts[65-79]
### Suggested fix
Split the `Promise.all` into two explicit steps (or two `try/catch` blocks) so you can throw:
- a “signature unreadable” message only when `readSignature` fails, and
- a “trusted keys unreadable” (or “could not load trusted Node.js release keys”) message when key loading fails.
Keep the existing error code if you want (`NODE_SHASUMS_SIGNATURE_INVALID`), but make the message accurately reflect which artifact was unreadable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Keys parsed repeatedly 🐞 Bug ➹ Performance
Description
The Rust pacquet verifier reparses all embedded Node release public keys on every SHASUMS
verification call. This adds avoidable CPU overhead during installs/resolutions that verify Node
SHASUMS multiple times in a process.
Code

pacquet/crates/crypto-shasums-file/src/lib.rs[R261-263]

+fn trusted_node_release_keys() -> Result<Vec<SignedPublicKey>, FetchVerifiedNodeShasumsError> {
+    NODE_RELEASE_KEYS.iter().map(read_node_release_key).collect()
+}
Evidence
The verification loop calls trusted_node_release_keys() and that function parses all embedded keys
via read_node_release_key() on each invocation, with no caching layer.

pacquet/crates/crypto-shasums-file/src/lib.rs[242-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`trusted_node_release_keys()` constructs a fresh `Vec<SignedPublicKey>` each time by parsing every armored key, and `is_signed_by_trusted_node_release_key()` calls it for every verification. This can be cached safely since the embedded key list is static.
## Issue Context
- TS implementation caches parsed key packets for the bundled key set.
- Rust path currently reparses on each verification.
## Fix Focus Areas
- pacquet/crates/crypto-shasums-file/src/lib.rs[242-263]
## Implementation notes
- Introduce a static cache (e.g. `std::sync::OnceLock<Vec<SignedPublicKey>>` or `once_cell::sync::Lazy`).
- Parse `NODE_RELEASE_KEYS` once into the cache and iterate borrowed keys during verification.
- Ensure parse failures are surfaced as `FetchVerifiedNodeShasumsError` (you may need a dedicated error variant for key-parse failures rather than reusing `SignatureUnreadable`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Verify Node.js runtime SHASUMS via OpenPGP signature (trusted release keys)
🐞 Bug fix ✨ Enhancement ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Verify downloaded Node.js runtime SHASUMS256.txt via detached OpenPGP signature.
• Embed/pin Node release public keys and gate releases on keylist freshness.
• Verify only release channel mirrors; keep hardcoded musl mirror trusted over TLS.
Diagram
graph TD
  A["Node runtime resolver"] --> B["SHASUMS fetch"] --> C{Signature verify?}
  C -->|"release channel"| D["OpenPGP verify"] --> E["Embedded Node keys"]
  C -->|"non-release / musl"| F["TLS trust only"]
  D --> G["Node mirror (configurable)"]
  F --> H["Unofficial musl mirror"]
  I["Release workflow"] --> J["Keys sync check"] --> E

  subgraph Legend
    direction LR
    _comp["Component"] ~~~ _dec{"Decision"} ~~~ _ext[["External source"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always fetch SHASUMS(+sig) from nodejs.org, regardless of mirror
  • ➕ Removes dependence on mirror serving the signature file correctly
  • ➕ Simplifies threat model: mirror only supplies tarball, not trust anchor
  • ➕ Avoids requiring embedded key updates if Node rotates signing infra but keeps publishing signatures
  • ➖ Breaks mirror-only/offline/proxy setups that expect all artifacts from the mirror
  • ➖ Adds extra network dependency/latency and potentially different CDN reachability than the mirror
  • ➖ Still requires trusting HTTPS to nodejs.org (though less risky than mirror-configured source)
2. Pin trusted key fingerprints only (download keys on demand)
  • ➕ Smaller repository footprint than embedding full armored keys
  • ➕ Allows key material refresh without PR churn if fetched from a canonical source
  • ➖ Reintroduces a fetch-time trust problem for key material unless a separate trust anchor is used
  • ➖ More runtime I/O and failure modes (key fetch failures block installs)
  • ➖ Harder to make fully hermetic/reproducible
3. Use a modern transparency/supply-chain scheme (Sigstore/TUF) for runtime artifacts
  • ➕ Stronger ecosystem guarantees and auditability than ad-hoc PGP flows
  • ➕ Potentially better rotation/revocation story
  • ➖ Not available for Node release artifacts today in a way pnpm can rely on
  • ➖ Much larger implementation and operational complexity

Recommendation: The PR’s approach (verify SHASUMS256.txt.sig against pinned Node release keys) is the best pragmatic fix for the mirror-configurable integrity loop. It preserves mirror compatibility for faithful mirrors, blocks malicious mirrors without requiring new upstream infrastructure, and adds a release-time guard to keep embedded trust roots current. The main alternative worth considering is fetching SHASUMS(+sig) from nodejs.org regardless of mirror; however, that would be a behavioral change that undermines mirror-only environments.

Grey Divider

File Changes

Enhancement (1)
index.ts Export verified SHASUMS fetcher and add verified SHASUMS file API +21/-1

Export verified SHASUMS fetcher and add verified SHASUMS file API

• Exports fetchVerifiedNodeShasums and adds fetchVerifiedNodeShasumsFile for consumers needing verified SHASUMS parsing. Refactors raw SHASUMS fetching to reuse a new parseShasumsFile helper.

crypto/shasums-file/src/index.ts


Bug fix (2)
verifyNodeShasums.ts Implement detached SHASUMS signature verification against pinned keys +120/-0

Implement detached SHASUMS signature verification against pinned keys

• Adds fetchVerifiedNodeShasums to fetch SHASUMS256.txt and SHASUMS256.txt.sig and verify the detached signature using openpgp key packets. Uses a packet-level verification path to avoid OpenPGP key-validity-window pitfalls with re-certified keys.

crypto/shasums-file/src/verifyNodeShasums.ts


index.ts Verify mirror-provided SHASUMS for release channel Node runtimes +18/-8

Verify mirror-provided SHASUMS for release channel Node runtimes

• Switches integrity fetching to use fetchVerifiedNodeShasumsFile when the Node mirror is repository-configurable and the channel is 'release'. Skips verification for unsigned channels and for the hardcoded unofficial musl mirror (trusted over TLS).

engine/runtime/node-resolver/src/index.ts


Tests (1)
verifyNodeShasums.ts Add unit tests for SHASUMS signature verification +67/-0

Add unit tests for SHASUMS signature verification

• Adds tests covering valid signatures, untrusted signing keys, tampered SHASUMS content, and missing .sig. Uses openpgp-generated test keys and a simple mocked fetch implementation.

crypto/shasums-file/test/verifyNodeShasums.ts


Documentation (1)
verify-node-runtime-shasums.md Changeset documenting Node runtime SHASUMS signature verification +13/-0

Changeset documenting Node runtime SHASUMS signature verification

• Adds a changeset describing the security issue (mirror-configurable SHASUMS circular trust) and the mitigation (OpenPGP signature verification against embedded Node release keys). Notes musl mirror exception.

.changeset/verify-node-runtime-shasums.md


Other (8)
create-release-pr.yml Gate releases on embedded Node release keys freshness +7/-0

Gate releases on embedded Node release keys freshness

• Adds a step to fail release PR creation if embedded Node release keys drift from nodejs/release-keys, preventing silent breakage when new signers are added upstream.

.github/workflows/create-release-pr.yml


package.json Add OpenPGP dependencies for SHASUMS verification +3/-1

Add OpenPGP dependencies for SHASUMS verification

• Introduces openpgp as a runtime dependency and @openpgp/web-stream-tools as a dev dependency to support signature verification and test tooling.

crypto/shasums-file/package.json


update-node-release-keys.mjs Add script to mirror Node release keys into the repo +74/-0

Add script to mirror Node release keys into the repo

• Adds a script to check/update the generated embedded key list from the canonical nodejs/release-keys repository. Supports a CI/check mode and an explicit update mode.

crypto/shasums-file/scripts/update-node-release-keys.mjs


nodeReleaseKeys.ts Embed generated Node.js release team OpenPGP public keys +122/-0

Embed generated Node.js release team OpenPGP public keys

• Adds a generated TypeScript file containing the pinned Node.js release public keys used to verify SHASUMS signatures at runtime. Marked as generated and excluded from spellchecking.

crypto/shasums-file/src/nodeReleaseKeys.ts


cspell.json Ignore generated Node keys file and add OpenPGP-related words +7/-0

Ignore generated Node keys file and add OpenPGP-related words

• Excludes nodeReleaseKeys.ts/d.ts from spellchecking and adds dictionary entries for openpgp and subkey terminology.

cspell.json


package.json Add workspace scripts to check/update embedded Node release keys +2/-0

Add workspace scripts to check/update embedded Node release keys

• Adds check:node-release-keys and update:node-release-keys scripts to run the key mirroring script in check or update mode.

package.json


pnpm-lock.yaml Lock new OpenPGP dependencies +42/-0

Lock new OpenPGP dependencies

• Adds openpgp and @openpgp/web-stream-tools to the lockfile, including catalog entries and resolved package metadata.

pnpm-lock.yaml


pnpm-workspace.yaml Add OpenPGP packages to workspace catalog +2/-0

Add OpenPGP packages to workspace catalog

• Adds openpgp and @openpgp/web-stream-tools to the workspace catalog for consistent dependency management across packages.

pnpm-workspace.yaml


Grey Divider

Qodo Logo

Comment thread crypto/shasums-file/scripts/update-node-release-keys.mjs Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7a27f09

@zkochan

zkochan commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Thanks @qodo — good catch on the revoked-key gap. Fixed in 7a27f09: the keys freshness check (update-node-release-keys.mjs) now fails when an embedded key is no longer in the canonical nodejs/release-keys list (removed/revoked), not just when a canonical key is missing. So a revoked Node release key can no longer stay in the embedded trust set. (--update already rewrites to exactly the canonical set, so it drops such keys.)

The other items in the summary (alternative approaches, walkthrough) were informational; the chosen pinned-keys + packet-level verification approach is intentional.


Written by an agent (Claude Code, claude-opus-4-8).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
engine/runtime/node-resolver/src/index.ts (1)

99-99: ⚡ Quick win

Consider refactoring to use an options object.

As per coding guidelines, functions should be limited to two or three arguments, with more parameters grouped into a single options object. readNodeAssets now has four parameters.

♻️ Proposed refactor
-async function readNodeAssets (fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string, releaseChannel: string): Promise<PlatformAssetResolution[]> {
+async function readNodeAssets (
+  fetch: FetchFromRegistry,
+  opts: { nodeMirrorBaseUrl: string, version: string, releaseChannel: string }
+): Promise<PlatformAssetResolution[]> {
+  const { nodeMirrorBaseUrl, version, releaseChannel } = opts

Update the call site at line 66:

-  const variants = await readNodeAssets(ctx.fetchFromRegistry, nodeMirrorBaseUrl, version, releaseChannel)
+  const variants = await readNodeAssets(ctx.fetchFromRegistry, { nodeMirrorBaseUrl, version, releaseChannel })

And the musl-variant call at line 113:

-      const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, muslOnly: true, verifySignature: false })
+      const muslAssets = await readNodeAssetsFromMirror(fetch, { nodeMirrorBaseUrl: UNOFFICIAL_NODE_MIRROR_BASE_URL, version, releaseChannel, muslOnly: true, verifySignature: false })

Based on coding guidelines: "limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/runtime/node-resolver/src/index.ts` at line 99, Refactor
readNodeAssets to accept a single options object instead of four positional
parameters: replace the signature async function readNodeAssets(fetch:
FetchFromRegistry, nodeMirrorBaseUrl: string, version: string, releaseChannel:
string) with async function readNodeAssets(opts: { fetch: FetchFromRegistry;
nodeMirrorBaseUrl: string; version: string; releaseChannel: string }):
Promise<PlatformAssetResolution[]> and update the implementation to use
opts.fetch, opts.nodeMirrorBaseUrl, opts.version and opts.releaseChannel; then
update every call site of readNodeAssets (including the standard and
musl-variant calls) to pass an object with those named properties rather than
four separate arguments, and adjust any type imports or usage accordingly.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@engine/runtime/node-resolver/src/index.ts`:
- Line 99: Refactor readNodeAssets to accept a single options object instead of
four positional parameters: replace the signature async function
readNodeAssets(fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version:
string, releaseChannel: string) with async function readNodeAssets(opts: {
fetch: FetchFromRegistry; nodeMirrorBaseUrl: string; version: string;
releaseChannel: string }): Promise<PlatformAssetResolution[]> and update the
implementation to use opts.fetch, opts.nodeMirrorBaseUrl, opts.version and
opts.releaseChannel; then update every call site of readNodeAssets (including
the standard and musl-variant calls) to pass an object with those named
properties rather than four separate arguments, and adjust any type imports or
usage accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b59cb51-b1af-4313-9f4a-590bce2dc28a

📥 Commits

Reviewing files that changed from the base of the PR and between 230df57 and 43ed90d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • .changeset/verify-node-runtime-shasums.md
  • .github/workflows/create-release-pr.yml
  • crypto/shasums-file/package.json
  • crypto/shasums-file/scripts/update-node-release-keys.mjs
  • crypto/shasums-file/src/index.ts
  • crypto/shasums-file/src/nodeReleaseKeys.ts
  • crypto/shasums-file/src/verifyNodeShasums.ts
  • crypto/shasums-file/test/verifyNodeShasums.ts
  • cspell.json
  • engine/runtime/node-resolver/src/index.ts
  • package.json
  • pnpm-workspace.yaml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • crypto/shasums-file/test/verifyNodeShasums.ts
  • crypto/shasums-file/src/index.ts
  • crypto/shasums-file/src/verifyNodeShasums.ts
  • engine/runtime/node-resolver/src/index.ts
🧠 Learnings (5)
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.

Applied to files:

  • package.json
  • crypto/shasums-file/package.json
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • crypto/shasums-file/test/verifyNodeShasums.ts
  • crypto/shasums-file/src/index.ts
  • crypto/shasums-file/src/verifyNodeShasums.ts
  • engine/runtime/node-resolver/src/index.ts
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • crypto/shasums-file/test/verifyNodeShasums.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • crypto/shasums-file/test/verifyNodeShasums.ts
  • crypto/shasums-file/src/index.ts
  • crypto/shasums-file/src/verifyNodeShasums.ts
  • engine/runtime/node-resolver/src/index.ts
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/verify-node-runtime-shasums.md
🔇 Additional comments (23)
pnpm-workspace.yaml (1)

244-245: LGTM!

crypto/shasums-file/package.json (1)

38-39: LGTM!

Also applies to: 43-43

package.json (1)

29-30: LGTM!

cspell.json (1)

2-5: LGTM!

Also applies to: 224-224, 353-354

crypto/shasums-file/scripts/update-node-release-keys.mjs (4)

1-18: LGTM!


19-44: LGTM!


46-55: LGTM!


57-75: LGTM!

crypto/shasums-file/src/verifyNodeShasums.ts (5)

1-23: LGTM!


25-63: LGTM!


65-88: LGTM!


90-109: LGTM!


111-120: LGTM!

crypto/shasums-file/src/index.ts (3)

6-8: LGTM!


15-20: LGTM!


22-34: LGTM!

crypto/shasums-file/test/verifyNodeShasums.ts (3)

1-27: LGTM!


29-48: LGTM!


50-67: LGTM!

engine/runtime/node-resolver/src/index.ts (2)

1-1: LGTM!


99-121: LGTM!

Also applies to: 129-136

.changeset/verify-node-runtime-shasums.md (1)

1-14: LGTM!

.github/workflows/create-release-pr.yml (1)

53-58: LGTM!

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e371499

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pacquet/crates/crypto-shasums-file/src/lib.rs (1)

171-171: ⚡ Quick win

Consider using from_utf8 with explicit error handling instead of from_utf8_lossy.

from_utf8_lossy silently replaces invalid UTF-8 bytes with U+FFFD replacement characters. If the verified SHASUMS file somehow contains non-UTF-8 bytes (e.g., due to a compromised mirror injecting binary data that still passes signature verification via a different attack vector), the replacement would corrupt hash values without raising an error. Using String::from_utf8(...).map_err(...) would surface such anomalies as explicit failures.

Suggested change
-    Ok(String::from_utf8_lossy(&shasums_bytes).into_owned())
+    String::from_utf8(shasums_bytes).map_err(|_| FetchVerifiedNodeShasumsError::SignatureInvalid {
+        url: shasums_url.to_string(),
+    })

Alternatively, add a dedicated error variant for invalid UTF-8 if you want a distinct diagnostic code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/crypto-shasums-file/src/lib.rs` at line 171, Replace the
silent UTF-8 lossiness at the end of the function that currently uses
String::from_utf8_lossy(&shasums_bytes).into_owned() with explicit UTF-8
validation: call String::from_utf8(shasums_bytes) and map any error into the
crate's error type (or add a new InvalidUtf8 error variant) so the function (the
code producing the Ok(...) in lib.rs) returns an explicit error on invalid UTF-8
instead of producing replacement characters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/crypto-shasums-file/src/lib.rs`:
- Around line 270-273: The debug-only assertion on the key fingerprint
(fingerprint_matches computed from key.fingerprint().to_string() and
trusted_key.fingerprint) must become a runtime check: replace
debug_assert!(fingerprint_matches) with an explicit conditional that returns an
error (or otherwise fails the function) when fingerprint_matches is false so the
function does not return Ok(key) for a mismatched fingerprint; keep the same
symbols (key, trusted_key, fingerprint_matches) and propagate an appropriate Err
from the enclosing function instead of allowing the key to be used.

---

Nitpick comments:
In `@pacquet/crates/crypto-shasums-file/src/lib.rs`:
- Line 171: Replace the silent UTF-8 lossiness at the end of the function that
currently uses String::from_utf8_lossy(&shasums_bytes).into_owned() with
explicit UTF-8 validation: call String::from_utf8(shasums_bytes) and map any
error into the crate's error type (or add a new InvalidUtf8 error variant) so
the function (the code producing the Ok(...) in lib.rs) returns an explicit
error on invalid UTF-8 instead of producing replacement characters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 823375b9-c651-45c5-9722-be9aad3a9631

📥 Commits

Reviewing files that changed from the base of the PR and between 7a27f09 and e371499.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml
  • crypto/shasums-file/scripts/update-node-release-keys.mjs
  • crypto/shasums-file/src/nodeReleaseKeys.ts
  • cspell.json
  • pacquet/crates/crypto-shasums-file/Cargo.toml
  • pacquet/crates/crypto-shasums-file/src/lib.rs
  • pacquet/crates/crypto-shasums-file/src/node_release_keys.rs
  • pacquet/crates/crypto-shasums-file/src/tests.rs
  • pacquet/crates/engine-runtime-node-resolver/Cargo.toml
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
✅ Files skipped from review due to trivial changes (1)
  • cspell.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • crypto/shasums-file/scripts/update-node-release-keys.mjs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Doc
  • GitHub Check: Format
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Dylint
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/Cargo.toml

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in [workspace.dependencies] in the root Cargo.toml before adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it

Files:

  • pacquet/crates/engine-runtime-node-resolver/Cargo.toml
  • pacquet/crates/crypto-shasums-file/Cargo.toml
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/crypto-shasums-file/src/tests.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
  • pacquet/crates/crypto-shasums-file/src/lib.rs
🧠 Learnings (4)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/crypto-shasums-file/src/tests.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
  • pacquet/crates/crypto-shasums-file/src/lib.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/crypto-shasums-file/src/tests.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
  • pacquet/crates/crypto-shasums-file/src/lib.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/crypto-shasums-file/src/tests.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
  • pacquet/crates/crypto-shasums-file/src/lib.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

  • pacquet/crates/crypto-shasums-file/src/tests.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs
  • pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs
  • pacquet/crates/crypto-shasums-file/src/lib.rs
🔇 Additional comments (14)
Cargo.toml (1)

133-133: LGTM!

pacquet/crates/crypto-shasums-file/Cargo.toml (1)

19-19: LGTM!

Also applies to: 23-23

pacquet/crates/crypto-shasums-file/src/lib.rs (4)

152-182: LGTM!


213-240: LGTM!


242-259: LGTM!


69-115: LGTM!

pacquet/crates/crypto-shasums-file/src/tests.rs (2)

101-165: LGTM!


167-231: LGTM!

pacquet/crates/engine-runtime-node-resolver/Cargo.toml (1)

29-29: LGTM!

pacquet/crates/engine-runtime-node-resolver/src/node_resolver.rs (3)

71-72: LGTM!


222-249: LGTM!


275-286: LGTM!

pacquet/crates/engine-runtime-node-resolver/src/node_resolver/tests.rs (2)

108-156: LGTM!


158-160: LGTM!

Comment thread pacquet/crates/crypto-shasums-file/src/lib.rs Outdated
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      5.1±0.10ms   847.5 KB/sec    1.01      5.1±0.22ms   843.2 KB/sec

@codecov-commenter

codecov-commenter commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.58824% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.68%. Comparing base (1017c36) to head (db86ff4).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/crypto-shasums-file/src/lib.rs 70.32% 27 Missing ⚠️
.../engine-runtime-node-resolver/src/node_resolver.rs 72.72% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12295      +/-   ##
==========================================
+ Coverage   87.54%   87.68%   +0.13%     
==========================================
  Files         288      288              
  Lines       34952    35050      +98     
==========================================
+ Hits        30599    30733     +134     
+ Misses       4353     4317      -36     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5425770

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2959415

zkochan added 7 commits June 9, 2026 23:40
When a repository requests a Node.js runtime (devEngines.runtime / useNodeVersion),
the download mirror is repository-configurable via `node-mirror:<channel>`. The
downloaded binary was only checked against `SHASUMS256.txt` fetched from that same
mirror — a circular check a malicious mirror satisfies by serving a tampered binary
plus a matching SHASUMS. pnpm then executes the binary (e.g. for lifecycle scripts).

pnpm now fetches `SHASUMS256.txt.sig` and verifies its detached OpenPGP signature
against the Node.js release team's public keys, embedded in the CLI. A mirror that
serves a tampered binary cannot also produce a valid signature.

- `@pnpm/crypto.shasums-file`: `fetchVerifiedNodeShasums` / `fetchVerifiedNodeShasumsFile`
  verify the signature (via openpgp) against the embedded keys.
- The keys live in a generated file kept in sync with the canonical
  nodejs/release-keys list; the create-release-pr workflow runs the check as a gate.
- The node-resolver verifies the configurable-mirror SHASUMS. The hardcoded
  unofficial-builds.nodejs.org musl mirror (not repo-configurable, signed by a
  different key) stays trusted over TLS.

Bun and Deno are unaffected: their download URLs are hardcoded to canonical GitHub.

Pacquet parity (engine-runtime-node-resolver has the same mirror logic) still needs
the equivalent Rust port; tracked as a follow-up.
…gned channels

Two CI failures in the Node.js runtime integration tests:

- Older releases (e.g. v22.0.0) failed to verify because `nodejs/release-keys`
  ships the *re-certified* key, whose self-signature postdates the release, so
  `openpgp.verify` (which validates the key as of the signature's creation time)
  rejects it. Since the trusted keys are pinned, verify the detached signature at
  the packet level against the matching key packet — the raw cryptographic check —
  bypassing OpenPGP's key-validity-window evaluation. Still rejects tampered
  content and signatures from untrusted keys.

- Pre-release channels (rc, nightly, …) have no SHASUMS256.txt.sig at all — Node
  only signs the `release` channel. Gate signature verification on the channel so
  those installs are not forced to fail. (They remain unverifiable, which is
  inherent: Node does not sign them.)

Verified against live nodejs.org across v18/v20/v22 release lines.
… canonical

The freshness check only failed when a canonical fingerprint was missing from
the embedded list. It now also fails when an embedded key is no longer in
nodejs/release-keys (a removed/revoked key), so a revoked Node release key cannot
stay in pnpm's trust set. --update already rewrites to exactly the canonical set.
@zkochan zkochan force-pushed the verify-node-runtime-shasums branch from 2959415 to fbeb182 Compare June 9, 2026 21:45
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit fbeb182

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0dd0db4

Comment thread pacquet/crates/crypto-shasums-file/src/lib.rs
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit db86ff4

@zkochan zkochan merged commit 3d50680 into main Jun 9, 2026
24 of 25 checks passed
@zkochan zkochan deleted the verify-node-runtime-shasums branch June 9, 2026 22:33
zkochan added a commit that referenced this pull request Jun 9, 2026
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:<channel> 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 #12295 to v10 (without the pacquet Rust port,
which does not exist on this branch).
zkochan added a commit that referenced this pull request Jun 10, 2026
* 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 #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 #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 #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 #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:<channel> 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 #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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants