@@ -7,6 +7,8 @@ const rpj = require('read-package-json-fast')
77const pickManifest = require ( 'npm-pick-manifest' )
88const ssri = require ( 'ssri' )
99const crypto = require ( 'crypto' )
10+ const npa = require ( 'npm-package-arg' )
11+ const { sigstore } = require ( 'sigstore' )
1012
1113// Corgis are cute. 🐕🐶
1214const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
@@ -203,7 +205,118 @@ class RegistryFetcher extends Fetcher {
203205 mani . _signatures = dist . signatures
204206 }
205207 }
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+ }
206318 }
319+
207320 this . package = mani
208321 return this . package
209322 }
0 commit comments