-
-
Notifications
You must be signed in to change notification settings - Fork 198
/
Copy pathnpmRegistryUtils.ts
190 lines (158 loc) · 7.49 KB
/
npmRegistryUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import * as sigstoreTuf from '@sigstore/tuf';
import {UsageError} from 'clipanion';
import assert from 'node:assert';
import * as crypto from 'node:crypto';
import * as path from 'node:path';
import defaultConfig from '../config.json';
import {shouldSkipIntegrityCheck} from './corepackUtils';
import * as debugUtils from './debugUtils';
import * as folderUtils from './folderUtils';
import * as httpUtils from './httpUtils';
// load abbreviated metadata as that's all we need for these calls
// see: https://github.com/npm/registry/blob/cfe04736f34db9274a780184d1cdb2fb3e4ead2a/docs/responses/package-metadata.md
export const DEFAULT_HEADERS: Record<string, string> = {
[`Accept`]: `application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8`,
};
export const DEFAULT_NPM_REGISTRY_URL = `https://registry.npmjs.org`;
export async function fetchAsJson(packageName: string, version?: string) {
const npmRegistryUrl = process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL;
if (process.env.COREPACK_ENABLE_NETWORK === `0`)
throw new UsageError(`Network access disabled by the environment; can't reach npm repository ${npmRegistryUrl}`);
const headers = {...DEFAULT_HEADERS};
if (`COREPACK_NPM_TOKEN` in process.env) {
headers.authorization = `Bearer ${process.env.COREPACK_NPM_TOKEN}`;
} else if (`COREPACK_NPM_USERNAME` in process.env
&& `COREPACK_NPM_PASSWORD` in process.env) {
const encodedCreds = Buffer.from(`${process.env.COREPACK_NPM_USERNAME}:${process.env.COREPACK_NPM_PASSWORD}`, `utf8`).toString(`base64`);
headers.authorization = `Basic ${encodedCreds}`;
}
return httpUtils.fetchAsJson(`${npmRegistryUrl}/${packageName}${version ? `/${version}` : ``}`, {headers});
}
interface KeyInfo {
keyid: string;
// base64 encoded DER SPKI
keyData: string;
}
async function fetchSigstoreTufKeys(): Promise<Array<KeyInfo> | null> {
// This follows the implementation for npm.
// See https://github.com/npm/cli/blob/3a80a7b7d168c23b5e297cba7b47ba5b9875934d/lib/utils/verify-signatures.js#L174
let keysRaw: string;
try {
// @ts-expect-error inject custom fetch into monkey-patched `tuf-js` module.
globalThis.tufJsFetch = async (input: string) => {
const agent = await httpUtils.getProxyAgent(input);
return await globalThis.fetch(input, {
dispatcher: agent,
});
};
const sigstoreTufClient = await sigstoreTuf.initTUF({
cachePath: path.join(folderUtils.getCorepackHomeFolder(), `_tuf`),
});
keysRaw = await sigstoreTufClient.getTarget(`registry.npmjs.org/keys.json`);
} catch (error) {
console.warn(`Warning: Failed to get signing keys from Sigstore TUF repo`, error);
return null;
}
// The format of the key file is undocumented but follows `PublicKey` from
// sigstore/protobuf-specs.
// See https://github.com/sigstore/protobuf-specs/blob/main/gen/pb-typescript/src/__generated__/sigstore_common.ts
const keysFromSigstore = JSON.parse(keysRaw) as {keys: Array<{keyId: string, publicKey: {rawBytes: string, keyDetails: string}}>};
return keysFromSigstore.keys.filter(key => {
if (key.publicKey.keyDetails === `PKIX_ECDSA_P256_SHA_256`) {
return true;
} else {
debugUtils.log(`Unsupported verification key type ${key.publicKey.keyDetails}`);
return false;
}
}).map(k => ({
keyid: k.keyId,
keyData: k.publicKey.rawBytes,
}));
}
async function getVerificationKeys(): Promise<Array<KeyInfo>> {
let keys: Array<{keyid: string, key: string}>;
if (process.env.COREPACK_INTEGRITY_KEYS) {
// We use the format of the `GET /-/npm/v1/keys` endpoint with `npm` instead
// of `keys` as the wrapping key.
const keysFromEnv = JSON.parse(process.env.COREPACK_INTEGRITY_KEYS) as {npm: Array<{keyid: string, key: string}>};
keys = keysFromEnv.npm;
debugUtils.log(`Using COREPACK_INTEGRITY_KEYS to verify signatures: ${keys.map(k => k.keyid).join(`, `)}`);
return keys.map(k => ({
keyid: k.keyid,
keyData: k.key,
}));
}
const sigstoreKeys = await fetchSigstoreTufKeys();
if (sigstoreKeys) {
debugUtils.log(`Using NPM keys from @sigstore/tuf to verify signatures: ${sigstoreKeys.map(k => k.keyid).join(`, `)}`);
return sigstoreKeys;
}
debugUtils.log(`Falling back to built-in npm verification keys`);
return defaultConfig.keys.npm.map(k => ({
keyid: k.keyid,
keyData: k.key,
}));
}
let verificationKeysCache: Promise<Array<KeyInfo>> | null = null;
export async function verifySignature({signatures, integrity, packageName, version}: {
signatures: Array<{keyid: string, sig: string}>;
integrity: string;
packageName: string;
version: string;
}) {
if (!Array.isArray(signatures) || !signatures.length) throw new Error(`No compatible signature found in package metadata`);
if (!verificationKeysCache)
verificationKeysCache = getVerificationKeys();
const keys = await verificationKeysCache;
const keyInfo = keys.find(({keyid}) => signatures.some(s => s.keyid === keyid));
if (keyInfo == null)
throw new Error(`Cannot find key to verify signature. signature keys: ${signatures.map(s => s.keyid)}, verification keys: ${keys.map(k => k.keyid)}`);
const signature = signatures.find(({keyid}) => keyid === keyInfo.keyid);
assert(signature);
const verifier = crypto.createVerify(`SHA256`);
const payload = `${packageName}@${version}:${integrity}`;
verifier.end(payload);
const key = crypto.createPublicKey({key: Buffer.from(keyInfo.keyData, `base64`), format: `der`, type: `spki`});
const valid = verifier.verify(key, signature.sig, `base64`);
if (!valid) {
throw new Error(
`Signature verification failed for ${payload} with key ${keyInfo.keyid}\n` +
`If you are using a custom registry you can set COREPACK_INTEGRITY_KEYS.`,
);
}
}
export async function fetchLatestStableVersion(packageName: string) {
const metadata = await fetchAsJson(packageName, `latest`);
const {version, dist: {integrity, signatures, shasum}} = metadata;
if (!shouldSkipIntegrityCheck()) {
try {
await verifySignature({
packageName, version,
integrity, signatures,
});
} catch (cause) {
// TODO: consider switching to `UsageError` when https://github.com/arcanis/clipanion/issues/157 is fixed
throw new Error(`Corepack cannot download the latest stable version of ${packageName}; you can disable signature verification by setting COREPACK_INTEGRITY_CHECK to 0 in your env, or instruct Corepack to use the latest stable release known by this version of Corepack by setting COREPACK_USE_LATEST to 0`, {cause});
}
}
return `${version}+${
integrity ?
`sha512.${Buffer.from(integrity.slice(7), `base64`).toString(`hex`)}` :
`sha1.${shasum}`
}`;
}
export async function fetchAvailableTags(packageName: string) {
const metadata = await fetchAsJson(packageName);
return metadata[`dist-tags`];
}
export async function fetchAvailableVersions(packageName: string) {
const metadata = await fetchAsJson(packageName);
return Object.keys(metadata.versions);
}
export async function fetchTarballURLAndSignature(packageName: string, version: string) {
const versionMetadata = await fetchAsJson(packageName, version);
const {tarball, signatures, integrity} = versionMetadata.dist;
if (tarball === undefined || !tarball.startsWith(`http`))
throw new Error(`${packageName}@${version} does not have a valid tarball.`);
return {tarball, signatures, integrity};
}