pacote 15.0.8 → 15.1.0

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/README.md CHANGED
@@ -172,7 +172,9 @@ resolved, and other properties, as they are determined.
172
172
  integrity signature of a manifest, if present. There must be a
173
173
  configured `_keys` entry in the config that is scoped to the
174
174
  registry the manifest is being fetched from.
175
-
175
+ * `verifyAttestations` A boolean that will make pacote verify Sigstore
176
+ attestations, if present. There must be a configured `_keys` entry in the
177
+ config that is scoped to the registry the manifest is being fetched from.
176
178
 
177
179
  ### Advanced API
178
180
 
package/lib/registry.js CHANGED
@@ -7,6 +7,8 @@ const rpj = require('read-package-json-fast')
7
7
  const pickManifest = require('npm-pick-manifest')
8
8
  const ssri = require('ssri')
9
9
  const crypto = require('crypto')
10
+ const npa = require('npm-package-arg')
11
+ const { sigstore } = require('sigstore')
10
12
 
11
13
  // Corgis are cute. 🐕🐶
12
14
  const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
@@ -203,7 +205,118 @@ class RegistryFetcher extends Fetcher {
203
205
  mani._signatures = dist.signatures
204
206
  }
205
207
  }
208
+
209
+ if (dist.attestations) {
210
+ if (this.opts.verifyAttestations) {
211
+ // Always fetch attestations from the current registry host
212
+ const attestationsPath = new URL(dist.attestations.url).pathname
213
+ const attestationsUrl = removeTrailingSlashes(this.registry) + attestationsPath
214
+ const res = await fetch(attestationsUrl, {
215
+ ...this.opts,
216
+ // disable integrity check for attestations json payload, we check the
217
+ // integrity in the verification steps below
218
+ integrity: null,
219
+ })
220
+ const { attestations } = await res.json()
221
+ const bundles = attestations.map(({ predicateType, bundle }) => {
222
+ const statement = JSON.parse(
223
+ Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString('utf8')
224
+ )
225
+ const keyid = bundle.dsseEnvelope.signatures[0].keyid
226
+ const signature = bundle.dsseEnvelope.signatures[0].sig
227
+
228
+ return {
229
+ predicateType,
230
+ bundle,
231
+ statement,
232
+ keyid,
233
+ signature,
234
+ }
235
+ })
236
+
237
+ const attestationKeyIds = bundles.map((b) => b.keyid).filter((k) => !!k)
238
+ const attestationRegistryKeys = (this.registryKeys || [])
239
+ .filter(key => attestationKeyIds.includes(key.keyid))
240
+ if (!attestationRegistryKeys.length) {
241
+ throw Object.assign(new Error(
242
+ `${mani._id} has attestations but no corresponding public key(s) can be found`
243
+ ), { code: 'EMISSINGSIGNATUREKEY' })
244
+ }
245
+
246
+ for (const { predicateType, bundle, keyid, signature, statement } of bundles) {
247
+ const publicKey = attestationRegistryKeys.find(key => key.keyid === keyid)
248
+ // Publish attestations have a keyid set and a valid public key must be found
249
+ if (keyid) {
250
+ if (!publicKey) {
251
+ throw Object.assign(new Error(
252
+ `${mani._id} has attestations with keyid: ${keyid} ` +
253
+ 'but no corresponding public key can be found'
254
+ ), { code: 'EMISSINGSIGNATUREKEY' })
255
+ }
256
+
257
+ const validPublicKey =
258
+ !publicKey.expires || (Date.parse(publicKey.expires) > Date.now())
259
+ if (!validPublicKey) {
260
+ throw Object.assign(new Error(
261
+ `${mani._id} has attestations with keyid: ${keyid} ` +
262
+ `but the corresponding public key has expired ${publicKey.expires}`
263
+ ), { code: 'EEXPIREDSIGNATUREKEY' })
264
+ }
265
+ }
266
+
267
+ const subject = {
268
+ name: statement.subject[0].name,
269
+ sha512: statement.subject[0].digest.sha512,
270
+ }
271
+
272
+ // Only type 'version' can be turned into a PURL
273
+ const purl = this.spec.type === 'version' ? npa.toPurl(this.spec) : this.spec
274
+ // Verify the statement subject matches the package, version
275
+ if (subject.name !== purl) {
276
+ throw Object.assign(new Error(
277
+ `${mani._id} package name and version (PURL): ${purl} ` +
278
+ `doesn't match what was signed: ${subject.name}`
279
+ ), { code: 'EATTESTATIONSUBJECT' })
280
+ }
281
+
282
+ // Verify the statement subject matches the tarball integrity
283
+ const integrityHexDigest = ssri.parse(this.integrity).hexDigest()
284
+ if (subject.sha512 !== integrityHexDigest) {
285
+ throw Object.assign(new Error(
286
+ `${mani._id} package integrity (hex digest): ` +
287
+ `${integrityHexDigest} ` +
288
+ `doesn't match what was signed: ${subject.sha512}`
289
+ ), { code: 'EATTESTATIONSUBJECT' })
290
+ }
291
+
292
+ try {
293
+ // Provenance attestations are signed with a signing certificate
294
+ // (including the key) so we don't need to return a public key.
295
+ //
296
+ // Publish attestations are signed with a keyid so we need to
297
+ // specify a public key from the keys endpoint: `registry-host.tld/-/npm/v1/keys`
298
+ const options = { keySelector: publicKey ? () => publicKey.pemkey : undefined }
299
+ await sigstore.verify(bundle, null, options)
300
+ } catch (e) {
301
+ throw Object.assign(new Error(
302
+ `${mani._id} failed to verify attestation: ${e.message}`
303
+ ), {
304
+ code: 'EATTESTATIONVERIFY',
305
+ predicateType,
306
+ keyid,
307
+ signature,
308
+ resolved: mani._resolved,
309
+ integrity: mani._integrity,
310
+ })
311
+ }
312
+ }
313
+ mani._attestations = dist.attestations
314
+ } else {
315
+ mani._attestations = dist.attestations
316
+ }
317
+ }
206
318
  }
319
+
207
320
  this.package = mani
208
321
  return this.package
209
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pacote",
3
- "version": "15.0.8",
3
+ "version": "15.1.0",
4
4
  "description": "JavaScript package downloader",
5
5
  "author": "GitHub Inc.",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "devDependencies": {
28
28
  "@npmcli/arborist": "^6.0.0 || ^6.0.0-pre.0",
29
29
  "@npmcli/eslint-config": "^4.0.0",
30
- "@npmcli/template-oss": "4.11.0",
30
+ "@npmcli/template-oss": "4.11.4",
31
31
  "hosted-git-info": "^6.0.0",
32
32
  "mutate-fs": "^2.1.1",
33
33
  "nock": "^13.2.4",
@@ -59,6 +59,7 @@
59
59
  "promise-retry": "^2.0.1",
60
60
  "read-package-json": "^6.0.0",
61
61
  "read-package-json-fast": "^3.0.0",
62
+ "sigstore": "^1.0.0",
62
63
  "ssri": "^10.0.0",
63
64
  "tar": "^6.1.11"
64
65
  },
@@ -71,7 +72,7 @@
71
72
  },
72
73
  "templateOSS": {
73
74
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
74
- "version": "4.11.0",
75
+ "version": "4.11.4",
75
76
  "windowsCI": false
76
77
  }
77
78
  }