Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
verify-signatures.js393 linesDownload Raw Back to utils
1const npmFetch = require('npm-registry-fetch')
2const localeCompare = require('@isaacs/string-locale-compare')('en')
3const npa = require('npm-package-arg')
4const pacote = require('pacote')
5const tufClient = require('@sigstore/tuf')
6const { log, output } = require('proc-log')
7
8const sortAlphabetically = (a, b) => localeCompare(a.name, b.name)
9
10class VerifySignatures {
11  constructor (tree, filterSet, npm, opts) {
12    this.tree = tree
13    this.filterSet = filterSet
14    this.npm = npm
15    this.opts = opts
16    this.keys = new Map()
17    this.invalid = []
18    this.missing = []
19    this.checkedPackages = new Set()
20    this.verified = []
21    this.auditedWithKeysCount = 0
22    this.verifiedSignatureCount = 0
23    this.verifiedAttestationCount = 0
24    this.exitCode = 0
25  }
26
27  async run () {
28    const start = process.hrtime.bigint()
29    const { default: pMap } = await import('p-map')
30
31    // Find all deps in tree
32    const { edges, registries } = this.getEdgesOut(this.tree.inventory.values(), this.filterSet)
33    if (edges.size === 0) {
34      throw new Error('found no installed dependencies to audit')
35    }
36
37    const tuf = await tufClient.initTUF({
38      cachePath: this.opts.tufCache,
39      retry: this.opts.retry,
40      timeout: this.opts.timeout,
41    })
42    await Promise.all([...registries].map(registry => this.setKeys({ registry, tuf })))
43
44    log.verbose('verifying registry signatures')
45    await pMap(edges, (e) => this.getVerifiedInfo(e), { concurrency: 20, stopOnError: true })
46
47    // Didn't find any dependencies that could be verified, e.g. only local deps, missing version, not on a registry etc.
48    if (!this.auditedWithKeysCount && !this.verifiedAttestationCount) {
49      throw new Error('found no dependencies to audit that were installed from ' +
50                      'a supported registry')
51    }
52
53    const invalid = this.invalid.sort(sortAlphabetically)
54    const missing = this.missing.sort(sortAlphabetically)
55
56    const hasNoInvalidOrMissing = invalid.length === 0 && missing.length === 0
57
58    if (!hasNoInvalidOrMissing) {
59      process.exitCode = 1
60    }
61
62    if (this.npm.config.get('json')) {
63      const result = { invalid, missing }
64      if (this.npm.config.get('include-attestations')) {
65        result.verified = this.verified
66      }
67      output.buffer(result)
68      return
69    }
70    const end = process.hrtime.bigint()
71    const elapsed = end - start
72
73    const auditedPlural = this.auditedWithKeysCount > 1 ? 's' : ''
74    const timing = `audited ${this.auditedWithKeysCount} package${auditedPlural} in ` +
75      `${Math.floor(Number(elapsed) / 1e9)}s`
76    output.standard(timing)
77    output.standard()
78
79    const verifiedBold = this.npm.chalk.bold('verified')
80    if (this.verifiedSignatureCount) {
81      if (this.verifiedSignatureCount === 1) {
82        output.standard(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`)
83      } else {
84        output.standard(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`)
85      }
86      output.standard()
87    }
88
89    if (this.verifiedAttestationCount) {
90      if (this.verifiedAttestationCount === 1) {
91        output.standard(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`)
92      } else {
93        output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`)
94      }
95      if (!this.npm.config.get('include-attestations')) {
96        output.standard('(use --json --include-attestations to view attestation details)')
97      }
98      output.standard()
99    }
100
101    if (missing.length) {
102      const missingClr = this.npm.chalk.redBright('missing')
103      if (missing.length === 1) {
104        output.standard(`1 package has a ${missingClr} registry signature but the registry is providing signing keys:`)
105      } else {
106        output.standard(`${missing.length} packages have ${missingClr} registry signatures but the registry is providing signing keys:`)
107      }
108      output.standard()
109      missing.map(m =>
110        output.standard(`${this.npm.chalk.red(`${m.name}@${m.version}`)} (${m.registry})`)
111      )
112    }
113
114    if (invalid.length) {
115      if (missing.length) {
116        output.standard()
117      }
118      const invalidClr = this.npm.chalk.redBright('invalid')
119      // We can have either invalid signatures or invalid provenance
120      const invalidSignatures = this.invalid.filter(i => i.code === 'EINTEGRITYSIGNATURE')
121      if (invalidSignatures.length) {
122        if (invalidSignatures.length === 1) {
123          output.standard(`1 package has an ${invalidClr} registry signature:`)
124        } else {
125          output.standard(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`)
126        }
127        output.standard()
128        invalidSignatures.map(i =>
129          output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)
130        )
131        output.standard()
132      }
133
134      const invalidAttestations = this.invalid.filter(i => i.code === 'EATTESTATIONVERIFY')
135      if (invalidAttestations.length) {
136        if (invalidAttestations.length === 1) {
137          output.standard(`1 package has an ${invalidClr} attestation:`)
138        } else {
139          output.standard(`${invalidAttestations.length} packages have ${invalidClr} attestations:`)
140        }
141        output.standard()
142        invalidAttestations.map(i =>
143          output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)
144        )
145        output.standard()
146      }
147
148      if (invalid.length === 1) {
149        output.standard(`Someone might have tampered with this package since it was published on the registry!`)
150      } else {
151        output.standard(`Someone might have tampered with these packages since they were published on the registry!`)
152      }
153      output.standard()
154    }
155  }
156
157  getEdgesOut (nodes, filterSet) {
158    const edges = new Set()
159    const registries = new Set()
160    for (const node of nodes) {
161      for (const edge of node.edgesOut.values()) {
162        const filteredOut =
163          edge.from
164            && filterSet
165            && filterSet.size > 0
166            && !filterSet.has(edge.from.target)
167
168        if (!filteredOut) {
169          const spec = this.getEdgeSpec(edge)
170          if (spec) {
171            // Prefetch and cache public keys from used registries
172            registries.add(this.getSpecRegistry(spec))
173          }
174          edges.add(edge)
175        }
176      }
177    }
178    return { edges, registries }
179  }
180
181  async setKeys ({ registry, tuf }) {
182    const { host, pathname } = new URL(registry)
183    // Strip any trailing slashes from pathname
184    const regKey = `${host}${pathname.replace(/\/$/, '')}/keys.json`
185    let keys = await tuf.getTarget(regKey)
186      .then((target) => JSON.parse(target))
187      .then(({ keys: ks }) => ks.map((key) => ({
188        ...key,
189        keyid: key.keyId,
190        pemkey: `-----BEGIN PUBLIC KEY-----\n${key.publicKey.rawBytes}\n-----END PUBLIC KEY-----`,
191        expires: key.publicKey.validFor.end || null,
192      }))).catch(err => {
193        if (err.code === 'TUF_FIND_TARGET_ERROR') {
194          return null
195        } else {
196          throw err
197        }
198      })
199
200    // If keys not found in Sigstore TUF repo, fall back to registry keys API
201    if (!keys) {
202      log.warn(`Fetching verification keys using TUF failed.  Fetching directly from ${registry}.`)
203      keys = await npmFetch.json('/-/npm/v1/keys', {
204        ...this.npm.flatOptions,
205        registry,
206      }).then(({ keys: ks }) => ks.map((key) => ({
207        ...key,
208        pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`,
209      }))).catch(err => {
210        if (err.code === 'E404' || err.code === 'E400') {
211          return null
212        } else {
213          throw err
214        }
215      })
216    }
217
218    if (keys) {
219      this.keys.set(registry, keys)
220    }
221  }
222
223  getEdgeType (edge) {
224    return edge.optional ? 'optionalDependencies'
225      : edge.peer ? 'peerDependencies'
226      : edge.dev ? 'devDependencies'
227      : 'dependencies'
228  }
229
230  getEdgeSpec (edge) {
231    let name = edge.name
232    try {
233      name = npa(edge.spec).subSpec.name
234    } catch {
235      // leave it as edge.name
236    }
237    try {
238      return npa(`${name}@${edge.spec}`)
239    } catch {
240      // Skip packages with invalid spec
241    }
242  }
243
244  buildRegistryConfig (registry) {
245    const keys = this.keys.get(registry) || []
246    const parsedRegistry = new URL(registry)
247    const regKey = `//${parsedRegistry.host}${parsedRegistry.pathname}`
248    return {
249      [`${regKey}:_keys`]: keys,
250    }
251  }
252
253  getSpecRegistry (spec) {
254    return npmFetch.pickRegistry(spec, this.npm.flatOptions)
255  }
256
257  getValidPackageInfo (edge) {
258    const type = this.getEdgeType(edge)
259    // Skip potentially optional packages that are not on disk, as these could
260    // be omitted during install
261    if (edge.error === 'MISSING' && type !== 'dependencies') {
262      return
263    }
264
265    const spec = this.getEdgeSpec(edge)
266    // Skip invalid version requirements
267    if (!spec) {
268      return
269    }
270    const node = edge.to || edge
271    const { version } = node.package || {}
272
273    if (node.isWorkspace || // Skip local workspaces packages
274        !version || // Skip packages that don't have an installed version, e.g. optional dependencies
275        !spec.registry) { // Skip if not from registry, e.g. git package
276      return
277    }
278
279    for (const omitType of this.npm.config.get('omit')) {
280      if (node[omitType]) {
281        return
282      }
283    }
284
285    return {
286      name: spec.name,
287      version,
288      type,
289      location: node.location,
290      registry: this.getSpecRegistry(spec),
291    }
292  }
293
294  async verifySignatures (name, version, registry) {
295    const {
296      _integrity: integrity,
297      _signatures,
298      _attestations,
299      _attestationBundles,
300      _resolved: resolved,
301    } = await pacote.manifest(`${name}@${version}`, {
302      verifySignatures: true,
303      verifyAttestations: true,
304      ...this.buildRegistryConfig(registry),
305      ...this.npm.flatOptions,
306    })
307    const signatures = _signatures || []
308    const result = {
309      integrity,
310      signatures,
311      attestations: _attestations,
312      attestationBundles: _attestationBundles,
313      resolved,
314    }
315    return result
316  }
317
318  async getVerifiedInfo (edge) {
319    const info = this.getValidPackageInfo(edge)
320    if (!info) {
321      return
322    }
323    const { name, version, location, registry, type } = info
324    if (this.checkedPackages.has(location)) {
325      // we already did or are doing this one
326      return
327    }
328    this.checkedPackages.add(location)
329
330    // We only "audit" or verify the signature, or the presence of it, on packages whose registry returns signing keys
331    const keys = this.keys.get(registry) || []
332    if (keys.length) {
333      this.auditedWithKeysCount += 1
334    }
335
336    try {
337      const { integrity, signatures, attestations, attestationBundles, resolved } =
338        await this.verifySignatures(name, version, registry)
339
340      // Currently we only care about missing signatures on registries that provide a public key
341      // We could make this configurable in the future with a strict/paranoid mode
342      if (signatures.length) {
343        this.verifiedSignatureCount += 1
344      } else if (keys.length) {
345        this.missing.push({
346          integrity,
347          location,
348          name,
349          registry,
350          resolved,
351          version,
352        })
353      }
354
355      // Track verified attestations separately to registry signatures, as all packages on registries with signing keys are expected to have registry signatures, but not all packages have provenance and publish attestations.
356      if (attestations) {
357        this.verifiedAttestationCount += 1
358        if (this.npm.config.get('include-attestations')) {
359          this.verified.push({
360            name,
361            version,
362            location,
363            registry,
364            attestations,
365            attestationBundles,
366          })
367        }
368      }
369    } catch (e) {
370      if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') {
371        this.invalid.push({
372          code: e.code,
373          message: e.message,
374          integrity: e.integrity,
375          keyid: e.keyid,
376          location,
377          name,
378          registry,
379          resolved: e.resolved,
380          signature: e.signature,
381          predicateType: e.predicateType,
382          type,
383          version,
384        })
385      } else {
386        throw e
387      }
388    }
389  }
390}
391
392module.exports = VerifySignatures
393 
codekingpro/portable-devtools · Team Ai