@@ -7,15 +7,14 @@ const npa = require('npm-package-arg')
77const rpj = require ( 'read-package-json-fast' )
88const pickManifest = require ( 'npm-pick-manifest' )
99const ssri = require ( 'ssri' )
10+ const crypto = require ( 'crypto' )
1011
1112// Corgis are cute. 🐕🐶
1213const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
1314const fullDoc = 'application/json'
1415
1516const fetch = require ( 'npm-registry-fetch' )
1617
17- // TODO: memoize reg requests, so we don't even have to check cache
18-
1918const _headers = Symbol ( '_headers' )
2019class RegistryFetcher extends Fetcher {
2120 constructor ( spec , opts ) {
@@ -39,28 +38,30 @@ class RegistryFetcher extends Fetcher {
3938 this . packumentUrl = removeTrailingSlashes ( this . registry ) + '/' +
4039 this . spec . escapedName
4140
41+ const parsed = new URL ( this . registry )
42+ const regKey = `//${ parsed . host } ${ parsed . pathname } `
43+ // unlike the nerf-darted auth keys, this one does *not* allow a mismatch
44+ // of trailing slashes. It must match exactly.
45+ if ( this . opts [ `${ regKey } :_keys` ] ) {
46+ this . registryKeys = this . opts [ `${ regKey } :_keys` ]
47+ }
48+
4249 // XXX pacote <=9 has some logic to ignore opts.resolved if
4350 // the resolved URL doesn't go to the same registry.
4451 // Consider reproducing that here, to throw away this.resolved
4552 // in that case.
4653 }
4754
48- resolve ( ) {
49- if ( this . resolved ) {
50- return Promise . resolve ( this . resolved )
51- }
52-
53- // fetching the manifest sets resolved and (usually) integrity
54- return this . manifest ( ) . then ( ( ) => {
55- if ( this . resolved ) {
56- return this . resolved
57- }
58-
55+ async resolve ( ) {
56+ // fetching the manifest sets resolved and (if present) integrity
57+ await this . manifest ( )
58+ if ( ! this . resolved ) {
5959 throw Object . assign (
6060 new Error ( 'Invalid package manifest: no `dist.tarball` field' ) ,
6161 { package : this . spec . toString ( ) }
6262 )
63- } )
63+ }
64+ return this . resolved
6465 }
6566
6667 [ _headers ] ( ) {
@@ -87,91 +88,127 @@ class RegistryFetcher extends Fetcher {
8788 // npm-registry-fetch the packument
8889 // set the appropriate header for corgis if fullMetadata isn't set
8990 // return the res.json() promise
90- const p = fetch ( this . packumentUrl , {
91- ...this . opts ,
92- headers : this [ _headers ] ( ) ,
93- spec : this . spec ,
94- // never check integrity for packuments themselves
95- integrity : null ,
96- } ) . then ( res => res . json ( ) . then ( packument => {
91+ try {
92+ const res = await fetch ( this . packumentUrl , {
93+ ...this . opts ,
94+ headers : this [ _headers ] ( ) ,
95+ spec : this . spec ,
96+ // never check integrity for packuments themselves
97+ integrity : null ,
98+ } )
99+ const packument = await res . json ( )
97100 packument . _cached = res . headers . has ( 'x-local-cache' )
98101 packument . _contentLength = + res . headers . get ( 'content-length' )
99102 if ( this . packumentCache ) {
100103 this . packumentCache . set ( this . packumentUrl , packument )
101104 }
102105 return packument
103- } ) ) . catch ( er => {
106+ } catch ( err ) {
104107 if ( this . packumentCache ) {
105108 this . packumentCache . delete ( this . packumentUrl )
106109 }
107- if ( er . code === 'E404' && ! this . fullMetadata ) {
108- // possible that corgis are not supported by this registry
109- this . fullMetadata = true
110- return this . packument ( )
110+ if ( err . code !== 'E404' || this . fullMetadata ) {
111+ throw err
111112 }
112- throw er
113- } )
114- if ( this . packumentCache ) {
115- this . packumentCache . set ( this . packumentUrl , p )
113+ // possible that corgis are not supported by this registry
114+ this . fullMetadata = true
115+ return this . packument ( )
116116 }
117- return p
118117 }
119118
120- manifest ( ) {
119+ async manifest ( ) {
121120 if ( this . package ) {
122- return Promise . resolve ( this . package )
121+ return this . package
123122 }
124123
125- return this . packument ( )
126- . then ( packument => pickManifest ( packument , this . spec . fetchSpec , {
127- ...this . opts ,
128- defaultTag : this . defaultTag ,
129- before : this . before ,
130- } ) /* XXX add ETARGET and E403 revalidation of cached packuments here */ )
131- . then ( mani => {
132- // add _resolved and _integrity from dist object
133- const { dist } = mani
134- if ( dist ) {
135- this . resolved = mani . _resolved = dist . tarball
136- mani . _from = this . from
137- const distIntegrity = dist . integrity ? ssri . parse ( dist . integrity )
138- : dist . shasum ? ssri . fromHex ( dist . shasum , 'sha1' , { ...this . opts } )
139- : null
140- if ( distIntegrity ) {
141- if ( ! this . integrity ) {
142- this . integrity = distIntegrity
143- } else if ( ! this . integrity . match ( distIntegrity ) ) {
144- // only bork if they have algos in common.
145- // otherwise we end up breaking if we have saved a sha512
146- // previously for the tarball, but the manifest only
147- // provides a sha1, which is possible for older publishes.
148- // Otherwise, this is almost certainly a case of holding it
149- // wrong, and will result in weird or insecure behavior
150- // later on when building package tree.
151- for ( const algo of Object . keys ( this . integrity ) ) {
152- if ( distIntegrity [ algo ] ) {
153- throw Object . assign ( new Error (
154- `Integrity checksum failed when using ${ algo } : ` +
155- `wanted ${ this . integrity } but got ${ distIntegrity } .`
156- ) , { code : 'EINTEGRITY' } )
157- }
158- }
159- // made it this far, the integrity is worthwhile. accept it.
160- // the setter here will take care of merging it into what we
161- // already had.
162- this . integrity = distIntegrity
124+ const packument = await this . packument ( )
125+ const mani = await pickManifest ( packument , this . spec . fetchSpec , {
126+ ...this . opts ,
127+ defaultTag : this . defaultTag ,
128+ before : this . before ,
129+ } )
130+ /* XXX add ETARGET and E403 revalidation of cached packuments here */
131+
132+ // add _resolved and _integrity from dist object
133+ const { dist } = mani
134+ if ( dist ) {
135+ this . resolved = mani . _resolved = dist . tarball
136+ mani . _from = this . from
137+ const distIntegrity = dist . integrity ? ssri . parse ( dist . integrity )
138+ : dist . shasum ? ssri . fromHex ( dist . shasum , 'sha1' , { ...this . opts } )
139+ : null
140+ if ( distIntegrity ) {
141+ if ( this . integrity && ! this . integrity . match ( distIntegrity ) ) {
142+ // only bork if they have algos in common.
143+ // otherwise we end up breaking if we have saved a sha512
144+ // previously for the tarball, but the manifest only
145+ // provides a sha1, which is possible for older publishes.
146+ // Otherwise, this is almost certainly a case of holding it
147+ // wrong, and will result in weird or insecure behavior
148+ // later on when building package tree.
149+ for ( const algo of Object . keys ( this . integrity ) ) {
150+ if ( distIntegrity [ algo ] ) {
151+ throw Object . assign ( new Error (
152+ `Integrity checksum failed when using ${ algo } : ` +
153+ `wanted ${ this . integrity } but got ${ distIntegrity } .`
154+ ) , { code : 'EINTEGRITY' } )
163155 }
164156 }
165157 }
166- if ( this . integrity ) {
167- mani . _integrity = String ( this . integrity )
168- if ( dist . signatures ) {
158+ // made it this far, the integrity is worthwhile. accept it.
159+ // the setter here will take care of merging it into what we already
160+ // had.
161+ this . integrity = distIntegrity
162+ }
163+ }
164+ if ( this . integrity ) {
165+ mani . _integrity = String ( this . integrity )
166+ if ( dist . signatures ) {
167+ if ( this . opts . verifySignatures ) {
168+ if ( this . registryKeys ) {
169+ // validate and throw on error, then set _signatures
170+ const message = `${ mani . _id } :${ mani . _integrity } `
171+ for ( const signature of dist . signatures ) {
172+ const publicKey = this . registryKeys . filter ( key => ( key . keyid === signature . keyid ) ) [ 0 ]
173+ if ( ! publicKey ) {
174+ throw Object . assign ( new Error (
175+ `${ mani . _id } has a signature with keyid: ${ signature . keyid } ` +
176+ 'but no corresponding public key can be found.'
177+ ) , { code : 'EMISSINGSIGNATUREKEY' } )
178+ }
179+ const validPublicKey =
180+ ! publicKey . expires || ( Date . parse ( publicKey . expires ) > Date . now ( ) )
181+ if ( ! validPublicKey ) {
182+ throw Object . assign ( new Error (
183+ `${ mani . _id } has a signature with keyid: ${ signature . keyid } ` +
184+ `but the corresponding public key has expired ${ publicKey . expires } `
185+ ) , { code : 'EEXPIREDSIGNATUREKEY' } )
186+ }
187+ const verifier = crypto . createVerify ( 'SHA256' )
188+ verifier . write ( message )
189+ verifier . end ( )
190+ const valid = verifier . verify (
191+ publicKey . pemkey ,
192+ signature . sig ,
193+ 'base64'
194+ )
195+ if ( ! valid ) {
196+ throw Object . assign ( new Error (
197+ 'Integrity checksum signature failed: ' +
198+ `key ${ publicKey . keyid } signature ${ signature . sig } `
199+ ) , { code : 'EINTEGRITYSIGNATURE' } )
200+ }
201+ }
169202 mani . _signatures = dist . signatures
170203 }
204+ // if no keys, don't set _signatures
205+ } else {
206+ mani . _signatures = dist . signatures
171207 }
172- this . package = rpj . normalize ( mani )
173- return this . package
174- } )
208+ }
209+ }
210+ this . package = rpj . normalize ( mani )
211+ return this . package
175212 }
176213
177214 [ _tarballFromResolved ] ( ) {
0 commit comments