@pnpm/deps.security.signatures 1101.2.3 → 1101.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/package.json +6 -6
- package/lib/index.d.ts +0 -1
- package/lib/npmSigningKeys.d.ts +0 -13
- package/lib/npmSigningKeys.js +0 -24
- package/lib/verifySignatures.d.ts +0 -89
- package/lib/verifySignatures.js +0 -328
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @pnpm/deps.security.signatures
|
|
2
|
+
|
|
3
|
+
## 1101.2.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies:
|
|
8
|
+
- @pnpm/network.fetch@1100.1.6
|
|
9
|
+
|
|
10
|
+
## 1101.2.4
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies:
|
|
15
|
+
- @pnpm/network.fetch@1100.1.5
|
|
16
|
+
|
|
17
|
+
## 1101.2.3
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [05b95ab]
|
|
22
|
+
- Updated dependencies [852d537]
|
|
23
|
+
- @pnpm/network.fetch@1100.1.4
|
|
24
|
+
- @pnpm/error@1100.0.1
|
|
25
|
+
|
|
26
|
+
## 1101.2.2
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- a31faa7: Updated dependency ranges. Notably:
|
|
31
|
+
|
|
32
|
+
- `@pnpm/logger` peer dependency range moved to `^1100.0.0`.
|
|
33
|
+
- `msgpackr` 1.11.8 → 2.0.4 (store index files remain byte-compatible in both directions).
|
|
34
|
+
- `open` ^7.4.2 → ^11.0.0, `memoize` ^10 → ^11, `cli-truncate` ^5 → ^6, `pidtree` ^0.6 → ^1.
|
|
35
|
+
- `@yarnpkg/core` 4.5.0 → 4.8.0, `@rushstack/worker-pool` 0.7.7 → 0.7.18, `@cyclonedx/cyclonedx-library` 10.0.0 → 10.1.0, `@pnpm/config.nerf-dart` ^1 → ^2, `@pnpm/log.group` 3.0.2 → 4.0.1, `@pnpm/util.lex-comparator` ^3 → ^4.
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [681b593]
|
|
38
|
+
- Updated dependencies [a31faa7]
|
|
39
|
+
- @pnpm/fetching.types@1100.0.2
|
|
40
|
+
- @pnpm/network.fetch@1100.1.3
|
|
41
|
+
|
|
42
|
+
## 1101.2.1
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- @pnpm/network.fetch@1100.1.2
|
|
47
|
+
|
|
48
|
+
## 1101.2.0
|
|
49
|
+
|
|
50
|
+
### Minor Changes
|
|
51
|
+
|
|
52
|
+
- 5f2bb9f: Security: pnpm now verifies the npm registry signature of a package-manager binary before spawning it, so a cloned repository cannot make pnpm download and execute an arbitrary native binary.
|
|
53
|
+
|
|
54
|
+
This covers two paths that select an executable from repository-controlled input:
|
|
55
|
+
|
|
56
|
+
- **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` opts in to pnpm's Rust install engine. pnpm now verifies that the installed `pacquet` shim and the host's `@pacquet/<platform>-<arch>` binary carry a valid npm registry signature for their exact `name@version`, and refuses to run pacquet (failing the command) if the signature does not verify or cannot be checked. The only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.
|
|
57
|
+
- **automatic version switch / `self-update`** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. pnpm now verifies the registry signature of `pnpm`, `@pnpm/exe`, and the host platform binary before installing/spawning them, and refuses to run an engine whose signature does not match a published, signed release. The check runs only on an actual download (store cache miss), so it does not add a network round trip to every command.
|
|
58
|
+
|
|
59
|
+
In both cases the signature is verified over the _installed_ integrity, against npm's public signing keys that ship embedded in the pnpm CLI (like corepack), so bytes substituted via a tampered lockfile or a repository-controlled registry fail verification — and a registry the user did not vouch for cannot supply its own signing keys. The signed packument is fetched from the configured registry, so an npm mirror works transparently. Verification fails closed: if it cannot be completed (for example, the registry is unreachable), the command fails rather than running an unverified binary. The embedded keys are kept current by a release-time check against npm's signing-keys endpoint.
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- @pnpm/network.fetch@1100.1.1
|
|
64
|
+
|
|
65
|
+
## 1101.1.6
|
|
66
|
+
|
|
67
|
+
### Patch Changes
|
|
68
|
+
|
|
69
|
+
- Updated dependencies [60a1eec]
|
|
70
|
+
- @pnpm/network.fetch@1100.1.0
|
|
71
|
+
|
|
72
|
+
## 1101.1.5
|
|
73
|
+
|
|
74
|
+
### Patch Changes
|
|
75
|
+
|
|
76
|
+
- Updated dependencies [b1fa2d5]
|
|
77
|
+
- @pnpm/network.fetch@1100.0.8
|
|
78
|
+
|
|
79
|
+
## 1101.1.4
|
|
80
|
+
|
|
81
|
+
### Patch Changes
|
|
82
|
+
|
|
83
|
+
- @pnpm/network.fetch@1100.0.7
|
|
84
|
+
|
|
85
|
+
## 1101.1.3
|
|
86
|
+
|
|
87
|
+
### Patch Changes
|
|
88
|
+
|
|
89
|
+
- @pnpm/network.fetch@1100.0.6
|
|
90
|
+
|
|
91
|
+
## 1101.1.2
|
|
92
|
+
|
|
93
|
+
### Patch Changes
|
|
94
|
+
|
|
95
|
+
- @pnpm/network.fetch@1100.0.5
|
|
96
|
+
|
|
97
|
+
## 1101.1.1
|
|
98
|
+
|
|
99
|
+
### Patch Changes
|
|
100
|
+
|
|
101
|
+
- Updated dependencies [18a464f]
|
|
102
|
+
- @pnpm/network.fetch@1100.0.4
|
|
103
|
+
|
|
104
|
+
## 1101.1.0
|
|
105
|
+
|
|
106
|
+
### Minor Changes
|
|
107
|
+
|
|
108
|
+
- 6ac06cb: Added `pnpm audit signatures` to verify ECDSA registry signatures for installed packages against keys from `/-/npm/v1/keys` [#7909](https://github.com/pnpm/pnpm/issues/7909). Scoped registries are respected, and registries without signing keys are skipped.
|
|
109
|
+
|
|
110
|
+
### Patch Changes
|
|
111
|
+
|
|
112
|
+
- Updated dependencies [20e7aff]
|
|
113
|
+
- @pnpm/network.fetch@1100.0.3
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/deps.security.signatures",
|
|
3
|
-
"version": "1101.2.
|
|
3
|
+
"version": "1101.2.5",
|
|
4
4
|
"description": "Verify package signatures from npm registries",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -29,19 +29,19 @@
|
|
|
29
29
|
"!*.map"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"p-limit": "^7.3.0",
|
|
33
|
-
"@pnpm/fetching.types": "1100.0.2",
|
|
34
32
|
"@pnpm/error": "1100.0.1",
|
|
35
|
-
"@pnpm/
|
|
33
|
+
"@pnpm/fetching.types": "1100.0.2",
|
|
34
|
+
"@pnpm/network.fetch": "1100.1.6",
|
|
35
|
+
"p-limit": "^7.3.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@pnpm/logger": "^1100.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@jest/globals": "30.4.1",
|
|
42
|
+
"@pnpm/deps.security.signatures": "1101.2.5",
|
|
42
43
|
"@pnpm/logger": "1100.0.0",
|
|
43
|
-
"@pnpm/testing.mock-agent": "1101.0.
|
|
44
|
-
"@pnpm/deps.security.signatures": "1101.2.3"
|
|
44
|
+
"@pnpm/testing.mock-agent": "1101.0.6"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=22.13"
|
package/lib/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './verifySignatures.js';
|
package/lib/npmSigningKeys.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export declare const NPM_SIGNING_KEYS: readonly [{
|
|
2
|
-
readonly expires: "2025-01-29T00:00:00.000Z";
|
|
3
|
-
readonly keyid: "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA";
|
|
4
|
-
readonly keytype: "ecdsa-sha2-nistp256";
|
|
5
|
-
readonly scheme: "ecdsa-sha2-nistp256";
|
|
6
|
-
readonly key: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==";
|
|
7
|
-
}, {
|
|
8
|
-
readonly expires: null;
|
|
9
|
-
readonly keyid: "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U";
|
|
10
|
-
readonly keytype: "ecdsa-sha2-nistp256";
|
|
11
|
-
readonly scheme: "ecdsa-sha2-nistp256";
|
|
12
|
-
readonly key: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==";
|
|
13
|
-
}];
|
package/lib/npmSigningKeys.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
// GENERATED — npm's public registry signing keys, mirrored from
|
|
3
|
-
// https://registry.npmjs.org/-/npm/v1/keys
|
|
4
|
-
//
|
|
5
|
-
// Refresh with: node deps/security/signatures/scripts/update-npm-signing-keys.mjs --update
|
|
6
|
-
// The release workflow runs `--check` and fails if these drift from npm, so a
|
|
7
|
-
// rotated key cannot silently break (or weaken) signature verification.
|
|
8
|
-
export const NPM_SIGNING_KEYS = [
|
|
9
|
-
{
|
|
10
|
-
"expires": "2025-01-29T00:00:00.000Z",
|
|
11
|
-
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
|
|
12
|
-
"keytype": "ecdsa-sha2-nistp256",
|
|
13
|
-
"scheme": "ecdsa-sha2-nistp256",
|
|
14
|
-
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"expires": null,
|
|
18
|
-
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
|
|
19
|
-
"keytype": "ecdsa-sha2-nistp256",
|
|
20
|
-
"scheme": "ecdsa-sha2-nistp256",
|
|
21
|
-
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=="
|
|
22
|
-
}
|
|
23
|
-
];
|
|
24
|
-
//# sourceMappingURL=npmSigningKeys.js.map
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { GetAuthHeader } from '@pnpm/fetching.types';
|
|
2
|
-
import { type CreateFetchFromRegistryOptions, type RetryTimeoutOptions } from '@pnpm/network.fetch';
|
|
3
|
-
export interface SignaturePackage {
|
|
4
|
-
name: string;
|
|
5
|
-
registry: string;
|
|
6
|
-
version: string;
|
|
7
|
-
}
|
|
8
|
-
export interface SignatureIssue extends SignaturePackage {
|
|
9
|
-
integrity?: string;
|
|
10
|
-
reason?: string;
|
|
11
|
-
resolved?: string;
|
|
12
|
-
}
|
|
13
|
-
export interface SignatureVerificationResult {
|
|
14
|
-
audited: number;
|
|
15
|
-
invalid: SignatureIssue[];
|
|
16
|
-
missing: SignatureIssue[];
|
|
17
|
-
verified: number;
|
|
18
|
-
}
|
|
19
|
-
export interface VerifySignaturesOptions extends CreateFetchFromRegistryOptions {
|
|
20
|
-
networkConcurrency?: number;
|
|
21
|
-
retry?: RetryTimeoutOptions;
|
|
22
|
-
timeout?: number;
|
|
23
|
-
}
|
|
24
|
-
interface RegistryKey {
|
|
25
|
-
expires: string | null;
|
|
26
|
-
key: string;
|
|
27
|
-
keyid: string;
|
|
28
|
-
keytype: string;
|
|
29
|
-
scheme: string;
|
|
30
|
-
}
|
|
31
|
-
export declare function verifySignatures(packages: SignaturePackage[], getAuthHeader: GetAuthHeader, opts: VerifySignaturesOptions): Promise<SignatureVerificationResult>;
|
|
32
|
-
export type { RegistryKey };
|
|
33
|
-
/**
|
|
34
|
-
* The trusted npm signing keys used to verify package-manager binaries before
|
|
35
|
-
* pnpm spawns them — npm's public keys embedded in the CLI. There is
|
|
36
|
-
* deliberately no way to override or disable them at runtime: a verification
|
|
37
|
-
* off-switch would be a footgun, and npm mirrors work without one (they proxy
|
|
38
|
-
* the same signed packument, which is verified against these keys). The keys
|
|
39
|
-
* are refreshed at release time by the update-npm-signing-keys script.
|
|
40
|
-
*/
|
|
41
|
-
export declare function getNpmSigningKeys(): RegistryKey[];
|
|
42
|
-
export interface InstalledPackageToVerify {
|
|
43
|
-
name: string;
|
|
44
|
-
/** The registry the package was installed from — the packument (and its signatures) is fetched from here. */
|
|
45
|
-
registry: string;
|
|
46
|
-
version: string;
|
|
47
|
-
/** Integrity of the bytes actually installed on disk (from the lockfile). */
|
|
48
|
-
integrity: string;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Why a package failed signature verification:
|
|
52
|
-
* - `invalid`: a registry signature is present but does not validate over the
|
|
53
|
-
* installed bytes — a strong tamper signal.
|
|
54
|
-
* - `absent`: the package/version is not on the (canonical) registry, or carries
|
|
55
|
-
* no signature — suspicious for a package that is expected to be signed.
|
|
56
|
-
* - `unreachable`: the trust root could not be consulted (registry advertised no
|
|
57
|
-
* signing keys, or the network request failed) — typically transient/offline,
|
|
58
|
-
* not evidence of tampering.
|
|
59
|
-
*/
|
|
60
|
-
export type SignatureFailureCategory = 'invalid' | 'absent' | 'unreachable';
|
|
61
|
-
export interface InstalledSignatureFailure {
|
|
62
|
-
name: string;
|
|
63
|
-
version: string;
|
|
64
|
-
reason: string;
|
|
65
|
-
category: SignatureFailureCategory;
|
|
66
|
-
}
|
|
67
|
-
export interface InstalledSignatureVerificationResult {
|
|
68
|
-
verified: boolean;
|
|
69
|
-
failures: InstalledSignatureFailure[];
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Verifies that the bytes installed on disk are exactly what the registry
|
|
73
|
-
* signed for `name@version`. The signed message is built from the
|
|
74
|
-
* caller-supplied installed {@link InstalledPackageToVerify.integrity}, not
|
|
75
|
-
* from the integrity in the freshly-fetched packument — so if the integrity
|
|
76
|
-
* on disk was tampered with (or fetched from a different registry), the
|
|
77
|
-
* registry's signature will not validate over it.
|
|
78
|
-
*
|
|
79
|
-
* Signatures are verified against the caller-supplied `trustedKeys` (npm's
|
|
80
|
-
* embedded public keys, see {@link getNpmSigningKeys}) rather than keys fetched
|
|
81
|
-
* from a registry — so a registry the caller cannot vouch for cannot answer with
|
|
82
|
-
* its own key pair. The packument (which carries the signatures) is fetched from
|
|
83
|
-
* each package's own registry; an npm mirror works transparently because it
|
|
84
|
-
* proxies the same signed packument.
|
|
85
|
-
*
|
|
86
|
-
* A package counts as a failure when the package is unsigned/unpublished, or
|
|
87
|
-
* when a signature is present but does not validate over the installed bytes.
|
|
88
|
-
*/
|
|
89
|
-
export declare function verifyInstalledPackageSignatures(packages: InstalledPackageToVerify[], trustedKeys: RegistryKey[], getAuthHeader: GetAuthHeader, opts: VerifySignaturesOptions): Promise<InstalledSignatureVerificationResult>;
|
package/lib/verifySignatures.js
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import url from 'node:url';
|
|
3
|
-
import util from 'node:util';
|
|
4
|
-
import { PnpmError } from '@pnpm/error';
|
|
5
|
-
import { createFetchFromRegistry } from '@pnpm/network.fetch';
|
|
6
|
-
import pLimit from 'p-limit';
|
|
7
|
-
import { NPM_SIGNING_KEYS } from './npmSigningKeys.js';
|
|
8
|
-
export async function verifySignatures(packages, getAuthHeader, opts) {
|
|
9
|
-
const registries = new Set(packages.map(({ registry }) => registry));
|
|
10
|
-
const keysByRegistry = await getKeysByRegistry(registries, getAuthHeader, opts);
|
|
11
|
-
const result = {
|
|
12
|
-
audited: 0,
|
|
13
|
-
invalid: [],
|
|
14
|
-
missing: [],
|
|
15
|
-
verified: 0,
|
|
16
|
-
};
|
|
17
|
-
// Registries without signing keys are not counted as audited: there is no
|
|
18
|
-
// registry trust root to verify against.
|
|
19
|
-
const packumentCache = new Map();
|
|
20
|
-
const limit = pLimit(opts.networkConcurrency ?? 16);
|
|
21
|
-
await Promise.all(packages.map((pkg) => limit(async () => {
|
|
22
|
-
const keys = keysByRegistry.get(pkg.registry) ?? [];
|
|
23
|
-
if (keys.length === 0)
|
|
24
|
-
return;
|
|
25
|
-
let version;
|
|
26
|
-
let publishedAt;
|
|
27
|
-
try {
|
|
28
|
-
const packument = await getPackument(pkg, getAuthHeader, opts, packumentCache);
|
|
29
|
-
if (!packument)
|
|
30
|
-
return;
|
|
31
|
-
result.audited++;
|
|
32
|
-
version = packument.versions?.[pkg.version];
|
|
33
|
-
publishedAt = packument.time?.[pkg.version];
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
result.invalid.push({ ...pkg, reason: util.types.isNativeError(err) ? err.message : String(err) });
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const integrity = version?.dist?.integrity;
|
|
40
|
-
const resolved = version?.dist?.tarball;
|
|
41
|
-
const rawSignatures = version?.dist?.signatures;
|
|
42
|
-
if (rawSignatures != null && !Array.isArray(rawSignatures)) {
|
|
43
|
-
result.invalid.push({ ...pkg, integrity, resolved, reason: `Malformed registry signatures metadata for ${pkg.name}@${pkg.version}` });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const signatures = rawSignatures ?? [];
|
|
47
|
-
if (!signatures.every(isPackageSignature)) {
|
|
48
|
-
result.invalid.push({ ...pkg, integrity, resolved, reason: `Malformed registry signatures metadata for ${pkg.name}@${pkg.version}` });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (!version) {
|
|
52
|
-
result.invalid.push({ ...pkg, reason: `Missing registry metadata for ${pkg.name}@${pkg.version}` });
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (!integrity) {
|
|
56
|
-
result.missing.push({ ...pkg, resolved });
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (signatures.length === 0) {
|
|
60
|
-
result.missing.push({ ...pkg, integrity, resolved });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const issue = verifyPackageSignatures({ ...pkg, integrity, publishedAt, resolved, signatures }, keys);
|
|
64
|
-
if (issue) {
|
|
65
|
-
result.invalid.push(issue);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
result.verified++;
|
|
69
|
-
})));
|
|
70
|
-
result.invalid.sort(sortIssue);
|
|
71
|
-
result.missing.sort(sortIssue);
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
async function getKeysByRegistry(registries, getAuthHeader, opts) {
|
|
75
|
-
const keysByRegistry = new Map();
|
|
76
|
-
await Promise.all(Array.from(registries, async (registry) => {
|
|
77
|
-
const keys = await fetchRegistryKeys(registry, getAuthHeader, opts);
|
|
78
|
-
keysByRegistry.set(registry, keys);
|
|
79
|
-
}));
|
|
80
|
-
return keysByRegistry;
|
|
81
|
-
}
|
|
82
|
-
async function fetchRegistryKeys(registry, getAuthHeader, opts) {
|
|
83
|
-
const registryUrl = registry.endsWith('/') ? registry : `${registry}/`;
|
|
84
|
-
const keysUrl = new url.URL('-/npm/v1/keys', registryUrl).toString();
|
|
85
|
-
const fetchFromRegistry = createFetchFromRegistry(opts);
|
|
86
|
-
const response = await fetchFromRegistry(keysUrl, {
|
|
87
|
-
authHeaderValue: getAuthHeader(registryUrl),
|
|
88
|
-
method: 'GET',
|
|
89
|
-
retry: opts.retry,
|
|
90
|
-
timeout: opts.timeout,
|
|
91
|
-
});
|
|
92
|
-
if (response.status === 404 || response.status === 400) {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
if (response.status !== 200) {
|
|
96
|
-
const code = 'AUDIT_SIGNATURE_KEYS_FETCH_FAIL';
|
|
97
|
-
const message = `The registry keys endpoint (at ${response.url}) responded with ${response.status}: ${await response.text()}`;
|
|
98
|
-
throw new PnpmError(code, message);
|
|
99
|
-
}
|
|
100
|
-
const body = await parseJsonResponse(response, 'AUDIT_SIGNATURE_KEYS_FETCH_FAIL', 'The registry keys endpoint');
|
|
101
|
-
if (!isRegistryKeysResponse(body)) {
|
|
102
|
-
const code = 'AUDIT_SIGNATURE_KEYS_FETCH_FAIL';
|
|
103
|
-
const message = `The registry keys endpoint (at ${response.url}) returned an unexpected body. Expected an object with a keys array; got: ${JSON.stringify(body)?.slice(0, 500) ?? String(body)}`;
|
|
104
|
-
throw new PnpmError(code, message);
|
|
105
|
-
}
|
|
106
|
-
// npm registry signing currently uses ECDSA P-256 keys. Sigstore provenance
|
|
107
|
-
// attestations are intentionally handled separately from this registry check.
|
|
108
|
-
return body.keys.filter(({ keytype, scheme }) => keytype === 'ecdsa-sha2-nistp256' && scheme === 'ecdsa-sha2-nistp256');
|
|
109
|
-
}
|
|
110
|
-
async function getPackument(pkg, getAuthHeader, opts, packumentCache) {
|
|
111
|
-
const cacheKey = `${pkg.registry}:${pkg.name}`;
|
|
112
|
-
let packument = packumentCache.get(cacheKey);
|
|
113
|
-
if (!packument) {
|
|
114
|
-
// Multiple installed versions share one full packument fetch.
|
|
115
|
-
packument = fetchPackument(pkg, getAuthHeader, opts);
|
|
116
|
-
packumentCache.set(cacheKey, packument);
|
|
117
|
-
}
|
|
118
|
-
return packument;
|
|
119
|
-
}
|
|
120
|
-
async function fetchPackument(pkg, getAuthHeader, opts) {
|
|
121
|
-
const registryUrl = pkg.registry.endsWith('/') ? pkg.registry : `${pkg.registry}/`;
|
|
122
|
-
const packumentUrl = toUri(pkg.name, registryUrl);
|
|
123
|
-
const fetchFromRegistry = createFetchFromRegistry(opts);
|
|
124
|
-
const response = await fetchFromRegistry(packumentUrl, {
|
|
125
|
-
authHeaderValue: getAuthHeader(registryUrl),
|
|
126
|
-
fullMetadata: true,
|
|
127
|
-
method: 'GET',
|
|
128
|
-
retry: opts.retry,
|
|
129
|
-
timeout: opts.timeout,
|
|
130
|
-
});
|
|
131
|
-
if (response.status === 404) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
if (response.status !== 200) {
|
|
135
|
-
const code = 'AUDIT_SIGNATURE_PACKUMENT_FETCH_FAIL';
|
|
136
|
-
const message = `The packument endpoint (at ${response.url}) responded with ${response.status}: ${await response.text()}`;
|
|
137
|
-
throw new PnpmError(code, message);
|
|
138
|
-
}
|
|
139
|
-
const body = await parseJsonResponse(response, 'AUDIT_SIGNATURE_PACKUMENT_FETCH_FAIL', 'The packument endpoint');
|
|
140
|
-
if (!isPackument(body)) {
|
|
141
|
-
const code = 'AUDIT_SIGNATURE_PACKUMENT_FETCH_FAIL';
|
|
142
|
-
const message = `The packument endpoint (at ${response.url}) returned an unexpected body. Expected an object with versions; got: ${JSON.stringify(body)?.slice(0, 500) ?? String(body)}`;
|
|
143
|
-
throw new PnpmError(code, message);
|
|
144
|
-
}
|
|
145
|
-
return body;
|
|
146
|
-
}
|
|
147
|
-
function verifyPackageSignatures(pkg, keys) {
|
|
148
|
-
// Registry signatures cover the package identity and content integrity.
|
|
149
|
-
const message = `${pkg.name}@${pkg.version}:${pkg.integrity}`;
|
|
150
|
-
const publishedTime = pkg.publishedAt ? Date.parse(pkg.publishedAt) : undefined;
|
|
151
|
-
// A package is accepted as soon as ONE signature made by a trusted key
|
|
152
|
-
// validates. Signatures from unknown/expired/invalid keys are recorded but do
|
|
153
|
-
// not on their own fail the package — otherwise a key rotation (a packument
|
|
154
|
-
// carrying multiple signatures) breaks, and a mirror could force a failure
|
|
155
|
-
// just by appending a junk signature. We fail only when no signature validates
|
|
156
|
-
// against a trusted key.
|
|
157
|
-
const failures = [];
|
|
158
|
-
for (const signature of pkg.signatures) {
|
|
159
|
-
const key = keys.find(({ keyid }) => keyid === signature.keyid);
|
|
160
|
-
if (!key) {
|
|
161
|
-
failures.push(`${pkg.name}@${pkg.version} has a registry signature with keyid ${signature.keyid} but no corresponding public key can be found`);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
// Key expiry is a consistency check, not a security boundary: the publish
|
|
165
|
-
// time comes from the same unauthenticated packument as the signatures, so
|
|
166
|
-
// a forger holding an expired trusted key could backdate it anyway. The
|
|
167
|
-
// signature verification below is what gates acceptance. That is why a
|
|
168
|
-
// missing publish time keeps the key usable instead of failing closed —
|
|
169
|
-
// the same trade-off npm's pacote makes by substituting a pre-expiry date.
|
|
170
|
-
if (key.expires && publishedTime != null && publishedTime >= Date.parse(key.expires)) {
|
|
171
|
-
failures.push(`${pkg.name}@${pkg.version} has a registry signature with keyid ${signature.keyid} but the corresponding public key has expired ${key.expires}`);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const pem = `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`;
|
|
175
|
-
// crypto.verify can throw on malformed PEM key material or signature bytes
|
|
176
|
-
// returned by the registry; treat any failure as an invalid signature so
|
|
177
|
-
// one bad key doesn't crash the whole audit.
|
|
178
|
-
let verified;
|
|
179
|
-
try {
|
|
180
|
-
const verifier = crypto.createVerify('SHA256');
|
|
181
|
-
verifier.write(message);
|
|
182
|
-
verifier.end();
|
|
183
|
-
verified = verifier.verify(pem, signature.sig, 'base64');
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
verified = false;
|
|
187
|
-
}
|
|
188
|
-
if (verified)
|
|
189
|
-
return undefined;
|
|
190
|
-
failures.push(`${pkg.name}@${pkg.version} has an invalid registry signature with keyid ${signature.keyid}`);
|
|
191
|
-
}
|
|
192
|
-
return toSignatureIssue(pkg, pickMostTellingFailure(pkg, failures));
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* The reason to surface when no signature validated against a trusted key.
|
|
196
|
-
* Prefer an invalid signature from a known key (a tamper signal) over an
|
|
197
|
-
* unknown-key or expiry reason, since unknown keys may just be junk a mirror
|
|
198
|
-
* appended.
|
|
199
|
-
*/
|
|
200
|
-
function pickMostTellingFailure(pkg, failures) {
|
|
201
|
-
if (failures.length === 0) {
|
|
202
|
-
return `${pkg.name}@${pkg.version} has no registry signature from a trusted key`;
|
|
203
|
-
}
|
|
204
|
-
return failures.find((reason) => reason.includes('invalid registry signature')) ?? failures[0];
|
|
205
|
-
}
|
|
206
|
-
function toSignatureIssue(pkg, reason) {
|
|
207
|
-
return {
|
|
208
|
-
integrity: pkg.integrity,
|
|
209
|
-
name: pkg.name,
|
|
210
|
-
reason,
|
|
211
|
-
registry: pkg.registry,
|
|
212
|
-
resolved: pkg.resolved,
|
|
213
|
-
version: pkg.version,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
async function parseJsonResponse(response, errorCode, endpointDescription) {
|
|
217
|
-
const rawBody = await response.text();
|
|
218
|
-
try {
|
|
219
|
-
return JSON.parse(rawBody);
|
|
220
|
-
}
|
|
221
|
-
catch (err) {
|
|
222
|
-
const reason = util.types.isNativeError(err) ? err.message : String(err);
|
|
223
|
-
throw new PnpmError(errorCode, `${endpointDescription} (at ${response.url}) returned invalid JSON: ${reason}. Response body: ${rawBody.slice(0, 500)}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function toUri(pkgName, registry) {
|
|
227
|
-
let encodedName;
|
|
228
|
-
if (pkgName[0] === '@') {
|
|
229
|
-
encodedName = `@${encodeURIComponent(pkgName.slice(1))}`;
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
encodedName = encodeURIComponent(pkgName);
|
|
233
|
-
}
|
|
234
|
-
return new url.URL(encodedName, registry.endsWith('/') ? registry : `${registry}/`).toString();
|
|
235
|
-
}
|
|
236
|
-
function isRegistryKeysResponse(body) {
|
|
237
|
-
return typeof body === 'object' && body != null &&
|
|
238
|
-
Array.isArray(body.keys) &&
|
|
239
|
-
body.keys.every((key) => typeof key === 'object' && key != null &&
|
|
240
|
-
typeof key.keyid === 'string' &&
|
|
241
|
-
typeof key.keytype === 'string' &&
|
|
242
|
-
typeof key.scheme === 'string' &&
|
|
243
|
-
typeof key.key === 'string' &&
|
|
244
|
-
(key.expires == null || typeof key.expires === 'string'));
|
|
245
|
-
}
|
|
246
|
-
function isPackument(body) {
|
|
247
|
-
return typeof body === 'object' && body != null && typeof body.versions === 'object' && body.versions != null;
|
|
248
|
-
}
|
|
249
|
-
function isPackageSignature(signature) {
|
|
250
|
-
return typeof signature === 'object' && signature != null &&
|
|
251
|
-
typeof signature.keyid === 'string' &&
|
|
252
|
-
typeof signature.sig === 'string';
|
|
253
|
-
}
|
|
254
|
-
function sortIssue(a, b) {
|
|
255
|
-
return `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`);
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* The trusted npm signing keys used to verify package-manager binaries before
|
|
259
|
-
* pnpm spawns them — npm's public keys embedded in the CLI. There is
|
|
260
|
-
* deliberately no way to override or disable them at runtime: a verification
|
|
261
|
-
* off-switch would be a footgun, and npm mirrors work without one (they proxy
|
|
262
|
-
* the same signed packument, which is verified against these keys). The keys
|
|
263
|
-
* are refreshed at release time by the update-npm-signing-keys script.
|
|
264
|
-
*/
|
|
265
|
-
export function getNpmSigningKeys() {
|
|
266
|
-
return NPM_SIGNING_KEYS.map((k) => ({ ...k }));
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Verifies that the bytes installed on disk are exactly what the registry
|
|
270
|
-
* signed for `name@version`. The signed message is built from the
|
|
271
|
-
* caller-supplied installed {@link InstalledPackageToVerify.integrity}, not
|
|
272
|
-
* from the integrity in the freshly-fetched packument — so if the integrity
|
|
273
|
-
* on disk was tampered with (or fetched from a different registry), the
|
|
274
|
-
* registry's signature will not validate over it.
|
|
275
|
-
*
|
|
276
|
-
* Signatures are verified against the caller-supplied `trustedKeys` (npm's
|
|
277
|
-
* embedded public keys, see {@link getNpmSigningKeys}) rather than keys fetched
|
|
278
|
-
* from a registry — so a registry the caller cannot vouch for cannot answer with
|
|
279
|
-
* its own key pair. The packument (which carries the signatures) is fetched from
|
|
280
|
-
* each package's own registry; an npm mirror works transparently because it
|
|
281
|
-
* proxies the same signed packument.
|
|
282
|
-
*
|
|
283
|
-
* A package counts as a failure when the package is unsigned/unpublished, or
|
|
284
|
-
* when a signature is present but does not validate over the installed bytes.
|
|
285
|
-
*/
|
|
286
|
-
export async function verifyInstalledPackageSignatures(packages, trustedKeys, getAuthHeader, opts) {
|
|
287
|
-
const packumentCache = new Map();
|
|
288
|
-
const limit = pLimit(opts.networkConcurrency ?? 16);
|
|
289
|
-
const failures = [];
|
|
290
|
-
await Promise.all(packages.map((pkg) => limit(async () => {
|
|
291
|
-
const failure = await findSignatureFailure(pkg, trustedKeys, getAuthHeader, opts, packumentCache);
|
|
292
|
-
if (failure != null) {
|
|
293
|
-
failures.push({ name: pkg.name, version: pkg.version, ...failure });
|
|
294
|
-
}
|
|
295
|
-
})));
|
|
296
|
-
failures.sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`));
|
|
297
|
-
return { verified: failures.length === 0, failures };
|
|
298
|
-
}
|
|
299
|
-
async function findSignatureFailure(pkg, trustedKeys, getAuthHeader, opts, packumentCache) {
|
|
300
|
-
let packument;
|
|
301
|
-
try {
|
|
302
|
-
packument = await getPackument(pkg, getAuthHeader, opts, packumentCache);
|
|
303
|
-
}
|
|
304
|
-
catch (err) {
|
|
305
|
-
return { reason: util.types.isNativeError(err) ? err.message : String(err), category: 'unreachable' };
|
|
306
|
-
}
|
|
307
|
-
if (!packument)
|
|
308
|
-
return { reason: `${pkg.name} is not published on ${pkg.registry}`, category: 'absent' };
|
|
309
|
-
const version = packument.versions?.[pkg.version];
|
|
310
|
-
if (!version)
|
|
311
|
-
return { reason: `${pkg.name}@${pkg.version} was not found on ${pkg.registry}`, category: 'absent' };
|
|
312
|
-
const rawSignatures = version.dist?.signatures;
|
|
313
|
-
if (rawSignatures != null && !Array.isArray(rawSignatures)) {
|
|
314
|
-
return { reason: `malformed registry signatures metadata for ${pkg.name}@${pkg.version}`, category: 'absent' };
|
|
315
|
-
}
|
|
316
|
-
const signatures = rawSignatures ?? [];
|
|
317
|
-
if (!signatures.every(isPackageSignature)) {
|
|
318
|
-
return { reason: `malformed registry signatures metadata for ${pkg.name}@${pkg.version}`, category: 'absent' };
|
|
319
|
-
}
|
|
320
|
-
if (signatures.length === 0) {
|
|
321
|
-
return { reason: `${pkg.name}@${pkg.version} has no registry signature`, category: 'absent' };
|
|
322
|
-
}
|
|
323
|
-
// The message is built from the installed integrity, so a signature only
|
|
324
|
-
// validates when the installed bytes match what the registry signed.
|
|
325
|
-
const issue = verifyPackageSignatures({ ...pkg, integrity: pkg.integrity, publishedAt: packument.time?.[pkg.version], signatures }, trustedKeys);
|
|
326
|
-
return issue == null ? undefined : { reason: issue.reason ?? 'invalid registry signature', category: 'invalid' };
|
|
327
|
-
}
|
|
328
|
-
//# sourceMappingURL=verifySignatures.js.map
|