feat(pacquet-cli): port audit signatures#12663
Conversation
Port pnpm's `pnpm audit signatures` registry-signature verification to pacquet. For every installed package, the package's own registry is queried for its signing keys (`/-/npm/v1/keys`) and full packument; the package is verified as soon as one of its `dist.signatures` validates over the message `name@version:integrity` against a trusted ECDSA-P256 key. Registries that advertise no signing keys are skipped (no trust root); a package whose registry provides keys but whose signature is absent is reported as missing, and one whose signature is present but does not validate as invalid (a tamper signal). Exit code 1 when any package is missing or invalid; `--json` emits the structured report. Mirrors pnpm's deps.security.signatures package and the auditSignatures command handler, including the per-package packument-error handling, the "one valid signature wins / prefer the tamper reason" logic, the key-expiry consistency check, encodeURIComponent-faithful packument URLs, the report wording, the error codes (ERR_PNPM_AUDIT_SIGNATURE_KEYS_FETCH_FAIL, ERR_PNPM_AUDIT_SIGNATURE_PACKUMENT_FETCH_FAIL, ERR_PNPM_AUDIT_NO_PACKAGES), and the JSON result shape. Adds the `p256` (RustCrypto) crate for ECDSA-P256/SHA-256 verification; it was already present transitively in the lockfile.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe CLI now supports ChangesSignature audit support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
PR Summary by QodoPort Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/cli/src/cli_args/audit.rs`:
- Around line 345-358: The audit path is losing each installed package’s
resolved registry because `lockfile_to_audit_request` collapses entries to
name/version pairs and `pick_registry_for_package` is then called with no
per-package resolution. Update the audit construction in `audit.rs` so
`SignaturePackage` is built while the lockfile still exposes
`SpecifierAndResolution`/resolution metadata, and pass that resolution into
`pick_registry_for_package` instead of `None`. Add a regression test for a
dependency resolved from a custom registry to verify the package is audited
against its own registry.
In `@pacquet/crates/cli/src/cli_args/audit/signatures.rs`:
- Around line 4-7: The module docs in signatures.rs reference pinned upstream
GitHub URLs that now 404, so update the two links in the doc comment to valid
upstream locations or a current commit hash. Keep the references aligned with
the existing pnpm parity docs for `@pnpm/deps.security.signatures` and the audit
signatures command, and verify the symbols in the comment remain accurate after
the URL update.
- Around line 193-214: The signature audit flow in `audit/signatures.rs` is
silently skipping packages when `fetch_packument` returns `Ok(None)` for a 404,
which lets `audit signatures` succeed without verifying those packages. Update
the `packuments` handling in the `SignatureVerificationResult` loop so that a
missing packument for a registry with signing keys is treated as an
invalid/per-package failure via `issue(...)`, not as a no-op; only skip packages
when `keys_by_registry` has no keys. Keep the behavior in `process_version` and
the `fetch_packument` call path aligned with pnpm semantics and fail closed for
missing or malformed registry data.
- Around line 539-540: The sanitize_body helper currently only truncates
attacker-controlled registry text but still passes through ANSI/control bytes
into user-facing diagnostics. Update sanitize_body in signatures::sanitize_body
to escape or remove control characters before truncation/rendering, so any
registry body displayed in audit output is safe to print in a terminal. Keep the
change localized to this helper and ensure all callers that render registry
bodies inherit the sanitization.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7c878cc5-0502-48fb-a15f-de968a91854a
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!Cargo.lock
📒 Files selected for processing (6)
Cargo.tomlpacquet/crates/cli/Cargo.tomlpacquet/crates/cli/src/cli_args/audit.rspacquet/crates/cli/src/cli_args/audit/signatures.rspacquet/crates/cli/src/cli_args/audit/signatures/tests.rspacquet/crates/cli/tests/audit.rs
|
Code review by qodo was updated up to the latest commit ab9aca2 |
Micro-Benchmark ResultsLinux |
- Hold the ThrottledClient permit through the keys/packument body read instead of dropping it early, so the network-concurrency bound holds under the audit fan-out. - Sanitize control characters in registry response bodies surfaced in diagnostics by reusing audit's sanitize_response_body helper, with a regression test for the keys-endpoint error path. - Fix the upstream pnpm doc links (the files live under pnpm11/).
|
Thanks for the reviews. Addressed the automated feedback in 153c997. Fixed
Declining (would diverge from pnpm — pacquet's cardinal rule is to match the TS CLI exactly)
Left the four declined threads open for a maintainer to weigh in on the parity rationale. Written by an agent (Claude Code, claude-opus-4-8). |
|
Code review by qodo was updated up to the latest commit 153c997 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12663 +/- ##
==========================================
+ Coverage 87.98% 88.00% +0.02%
==========================================
Files 352 354 +2
Lines 52478 53103 +625
==========================================
+ Hits 46171 46734 +563
- Misses 6307 6369 +62 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code review by qodo was updated up to the latest commit 153c997 |
…work errors The KeysNetwork/PackumentNetwork variants carried the raw reqwest::Error as a diagnostic source and printed it via Display, which can embed the request URL — leaking `user:pass@` userinfo from a credentialed registry URL into stderr/CI output. Store the error as a string already passed through redact_url_credentials instead, dropping the reqwest::Error source so neither the message nor the miette cause chain can leak. Adds an integration test asserting a credentialed registry URL does not leak its userinfo on a transport error.
|
Code review by qodo was updated up to the latest commit 527e661 |
1 similar comment
|
Code review by qodo was updated up to the latest commit 527e661 |
Integrated-Benchmark Report (Linux)Commit: Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.77667134318,
"stddev": 0.10863032445381617,
"median": 4.7290127125799994,
"user": 3.89471946,
"system": 3.5777531199999997,
"min": 4.66979062558,
"max": 4.96039502258,
"times": [
4.72549967258,
4.67012219358,
4.87748416258,
4.96039502258,
4.80205072458,
4.73252575258,
4.68801209258,
4.66979062558,
4.71408724558,
4.92674593958
]
},
{
"command": "pacquet@main",
"mean": 4.8062456539800005,
"stddev": 0.18939832200280338,
"median": 4.74041210108,
"user": 3.9380135600000004,
"system": 3.5515420199999994,
"min": 4.63797828258,
"max": 5.19897262958,
"times": [
4.74577390358,
5.19897262958,
4.63797828258,
4.66249963158,
5.04408111158,
4.69118728358,
4.65108750158,
4.76557798958,
4.93024790758,
4.73505029858
]
},
{
"command": "pnpr@HEAD",
"mean": 3.05539781178,
"stddev": 0.11387455926529064,
"median": 3.02266136608,
"user": 2.7249750600000007,
"system": 3.1210342199999994,
"min": 2.93696446958,
"max": 3.27378597558,
"times": [
2.93696446958,
3.05450249858,
3.27378597558,
3.03346451058,
2.96552894858,
3.13185016058,
2.99103847258,
3.01185822158,
3.20704836458,
2.94793649558
]
},
{
"command": "pnpr@main",
"mean": 3.0653909797800005,
"stddev": 0.1455711659103221,
"median": 3.04725675708,
"user": 2.7921319600000003,
"system": 3.0781005199999996,
"min": 2.90896153558,
"max": 3.3268727395799997,
"times": [
2.9455078975799998,
3.25643097058,
2.90896153558,
3.1027850995799997,
2.96088405058,
2.91084019058,
3.3268727395799997,
3.08888175958,
3.00563175458,
3.14711379958
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.67209119594,
"stddev": 0.019896503901143187,
"median": 0.66828133634,
"user": 0.41886304,
"system": 1.3598305199999998,
"min": 0.64616379584,
"max": 0.7112326088400001,
"times": [
0.68775924184,
0.67970805984,
0.67809048384,
0.65696030484,
0.65840990684,
0.6576101818400001,
0.65847218884,
0.6865051868400001,
0.64616379584,
0.7112326088400001
]
},
{
"command": "pacquet@main",
"mean": 0.6954308971400001,
"stddev": 0.09310415485022885,
"median": 0.66354601034,
"user": 0.39616573999999993,
"system": 1.36836902,
"min": 0.64913723284,
"max": 0.9556051828400001,
"times": [
0.66999546884,
0.6548723658400001,
0.6725629068400001,
0.66945447784,
0.6576375428400001,
0.64913723284,
0.6564101198400001,
0.9556051828400001,
0.71164825084,
0.65698542284
]
},
{
"command": "pnpr@HEAD",
"mean": 0.71840622004,
"stddev": 0.026952201241279235,
"median": 0.71402206534,
"user": 0.42604823999999997,
"system": 1.3741570200000002,
"min": 0.68401045884,
"max": 0.77286657284,
"times": [
0.7073682898400001,
0.68698607984,
0.6995438878400001,
0.74085550584,
0.77286657284,
0.73150432584,
0.7144665598400001,
0.68401045884,
0.73288294884,
0.7135775708400001
]
},
{
"command": "pnpr@main",
"mean": 0.76999530364,
"stddev": 0.0920302764894872,
"median": 0.7304392363400001,
"user": 0.4083236399999999,
"system": 1.40806222,
"min": 0.6990741298400001,
"max": 1.0141580938399999,
"times": [
0.7257197738400001,
0.79011445684,
0.7828436188400001,
0.72644657584,
0.6990741298400001,
0.7344318968400001,
0.7148135308400001,
1.0141580938399999,
0.79126416984,
0.7210867898400001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.8434060927200004,
"stddev": 0.0260432508511132,
"median": 4.84399815232,
"user": 3.8959783000000003,
"system": 3.48686654,
"min": 4.801624154820001,
"max": 4.89287067782,
"times": [
4.820819383820001,
4.83540369682,
4.89287067782,
4.854343211820001,
4.801624154820001,
4.861258808820001,
4.82911420782,
4.86048552082,
4.85259260782,
4.825548656820001
]
},
{
"command": "pacquet@main",
"mean": 4.869607389920001,
"stddev": 0.044569044710573755,
"median": 4.87937942082,
"user": 3.9518773000000005,
"system": 3.4660306400000005,
"min": 4.780721074820001,
"max": 4.937759642820001,
"times": [
4.780721074820001,
4.853138184820001,
4.8169723948200005,
4.87762079182,
4.937759642820001,
4.86579977782,
4.8827524468200005,
4.904989312820001,
4.8811380498200005,
4.895182222820001
]
},
{
"command": "pnpr@HEAD",
"mean": 3.27937211382,
"stddev": 0.10295841196803845,
"median": 3.24033375632,
"user": 2.8428201999999994,
"system": 3.27083784,
"min": 3.15107797882,
"max": 3.45348654482,
"times": [
3.15107797882,
3.40895081582,
3.20415666582,
3.45348654482,
3.33353612782,
3.36405077382,
3.23427111782,
3.24639639482,
3.19775683582,
3.20003788282
]
},
{
"command": "pnpr@main",
"mean": 3.2951446102199995,
"stddev": 0.0915350530521153,
"median": 3.27109635482,
"user": 2.8837923999999995,
"system": 3.27894314,
"min": 3.20135053282,
"max": 3.41749121582,
"times": [
3.30309730682,
3.21378447282,
3.20943573682,
3.20135053282,
3.21186305482,
3.23909540282,
3.41749121582,
3.41291614782,
3.33884044882,
3.40357178282
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.42787691502,
"stddev": 0.01593119892757247,
"median": 1.4294108586199998,
"user": 1.4287266799999998,
"system": 1.7506598,
"min": 1.40499030862,
"max": 1.45879294862,
"times": [
1.4407914126199999,
1.40499030862,
1.45879294862,
1.43427229862,
1.4267226686199999,
1.4298377576199999,
1.41109018562,
1.41089567762,
1.43239193262,
1.42898395962
]
},
{
"command": "pacquet@main",
"mean": 1.44312442692,
"stddev": 0.04849395894004138,
"median": 1.43601571662,
"user": 1.3959339800000001,
"system": 1.7861824,
"min": 1.4041857846199999,
"max": 1.5763690526199998,
"times": [
1.4041857846199999,
1.5763690526199998,
1.43725716062,
1.4399422336199998,
1.43763885262,
1.42227406362,
1.43477427262,
1.44353959562,
1.41484013762,
1.42042311562
]
},
{
"command": "pnpr@HEAD",
"mean": 1.4892628782200001,
"stddev": 0.08646838683513486,
"median": 1.46368009562,
"user": 0.6341421799999999,
"system": 1.5882144,
"min": 1.43427252262,
"max": 1.73116201162,
"times": [
1.48176292462,
1.73116201162,
1.4749870106199998,
1.45285086662,
1.48531828462,
1.45579149562,
1.45048622062,
1.43427252262,
1.45442874962,
1.47156869562
]
},
{
"command": "pnpr@main",
"mean": 1.46666715212,
"stddev": 0.050280346212067055,
"median": 1.45296389712,
"user": 0.6051898799999998,
"system": 1.5741695,
"min": 1.4324881376199998,
"max": 1.6032349346199999,
"times": [
1.46167100862,
1.6032349346199999,
1.4786662826199999,
1.43646706662,
1.4324881376199998,
1.45561145162,
1.43380680362,
1.45031634262,
1.44709003662,
1.46731945662
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.1324814292,
"stddev": 0.04027287736242253,
"median": 3.126894444,
"user": 1.91312358,
"system": 2.02428424,
"min": 3.0929927520000002,
"max": 3.209608193,
"times": [
3.209608193,
3.095381994,
3.0929927520000002,
3.102553586,
3.131888723,
3.096192662,
3.121900165,
3.146055579,
3.1396078800000002,
3.1886327580000002
]
},
{
"command": "pacquet@main",
"mean": 3.0495278430000003,
"stddev": 0.06406198912224452,
"median": 3.0531972085,
"user": 1.80710858,
"system": 1.99655174,
"min": 2.960083344,
"max": 3.1817865850000002,
"times": [
3.087597405,
3.017478718,
3.001776516,
3.05851908,
3.047875337,
3.093111951,
2.982593976,
3.1817865850000002,
3.064455518,
2.960083344
]
},
{
"command": "pnpr@HEAD",
"mean": 1.4123016696000001,
"stddev": 0.023433667894945477,
"median": 1.4164311595,
"user": 0.60104008,
"system": 1.5673545399999997,
"min": 1.37502625,
"max": 1.441555507,
"times": [
1.423920328,
1.441555507,
1.37502625,
1.408941991,
1.426988479,
1.429405145,
1.386394337,
1.440358763,
1.395078558,
1.395347338
]
},
{
"command": "pnpr@main",
"mean": 1.4542568374000002,
"stddev": 0.08964804228976557,
"median": 1.4222568525,
"user": 0.59544648,
"system": 1.57830784,
"min": 1.384366437,
"max": 1.687471762,
"times": [
1.499061367,
1.687471762,
1.460627219,
1.464911427,
1.413621051,
1.405068908,
1.430892654,
1.395959139,
1.384366437,
1.4005884100000001
]
}
]
} |
|
| Branch | pr/12663 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,843.41 ms(+2.20%)Baseline: 4,739.14 ms | 5,686.96 ms (85.17%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 3,132.48 ms(+3.82%)Baseline: 3,017.18 ms | 3,620.62 ms (86.52%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,427.88 ms(+6.42%)Baseline: 1,341.77 ms | 1,610.13 ms (88.68%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,776.67 ms(+2.01%)Baseline: 4,682.45 ms | 5,618.94 ms (85.01%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 672.09 ms(+5.30%)Baseline: 638.28 ms | 765.94 ms (87.75%) |
|
| Branch | pr/12663 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 3,279.37 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 1,412.30 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 1,489.26 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 3,055.40 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 718.41 ms |
Summary
Ports pnpm's
pnpm audit signaturescommand to pacquet. The TypeScript CLI already ships this command; this PR brings the Rust port to parity.For every installed package, the package's own registry is queried for its signing keys (
/-/npm/v1/keys) and full packument. A package is verified as soon as one of itsdist.signaturesvalidates — over the messagename@version:integrity— against a trusted ECDSA‑P256 key. Registries that advertise no signing keys are skipped (no trust root); a package whose registry provides keys but whose signature is absent is reported as missing, and one whose signature is present but does not validate as invalid (a tamper signal). Exit code is 1 when anything is missing or invalid, and--jsonemits the structured report.The port mirrors pnpm's
deps.security.signaturespackage and theauditSignaturescommand handler, including per‑package packument‑error handling, the "one valid signature wins / prefer the tamper reason" logic, the key‑expiry consistency check,encodeURIComponent‑faithful packument URLs, the report wording, the error codes (ERR_PNPM_AUDIT_SIGNATURE_KEYS_FETCH_FAIL,ERR_PNPM_AUDIT_SIGNATURE_PACKUMENT_FETCH_FAIL,ERR_PNPM_AUDIT_NO_PACKAGES), and the JSON result shape.ECDSA‑P256/SHA‑256 verification uses the
p256(RustCrypto) crate, which was already present transitively in the lockfile and is now a direct dependency ofpacquet-cli.Squash Commit Body
Checklist
pacquet/port, or the description notes what still needs porting.Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
New Features
pacquet audit signatures, verifying installed package signatures using registry ECDSA-P256 keys.Bug Fixes
audit signaturesargument/subcommand handling, rejecting extra or unknown operands with clear “unknown” messaging.Tests