File Explorer

/var/lang/lib/node_modules/npm/lib/utils

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

verify-signatures.js12.5 KB · 398 lines
const npmFetch = require('npm-registry-fetch')const localeCompare = require('@isaacs/string-locale-compare')('en')const npa = require('npm-package-arg')const pacote = require('pacote')const tufClient = require('@sigstore/tuf')const { log, output } = require('proc-log') const sortAlphabetically = (a, b) => localeCompare(a.name, b.name) class VerifySignatures {  constructor (tree, filterSet, npm, opts) {    this.tree = tree    this.filterSet = filterSet    this.npm = npm    this.opts = opts    this.keys = new Map()    this.invalid = []    this.missing = []    this.checkedPackages = new Set()    this.verified = []    this.auditedWithKeysCount = 0    this.verifiedSignatureCount = 0    this.verifiedAttestationCount = 0    this.exitCode = 0  }   async run () {    const start = process.hrtime.bigint()    const { default: pMap } = await import('p-map')     // Find all deps in tree    const { edges, registries } = this.getEdgesOut(this.tree.inventory.values(), this.filterSet)    if (edges.size === 0) {      throw new Error('found no installed dependencies to audit')    }     const tuf = await tufClient.initTUF({      cachePath: this.opts.tufCache,      retry: this.opts.retry,      timeout: this.opts.timeout,    })    await Promise.all([...registries].map(registry => this.setKeys({ registry, tuf })))     log.verbose('verifying registry signatures')    await pMap(edges, (e) => this.getVerifiedInfo(e), { concurrency: 20, stopOnError: true })     // Didn't find any dependencies that could be verified, e.g. only local deps, missing version, not on a registry etc.    if (!this.auditedWithKeysCount && !this.verifiedAttestationCount) {      throw new Error('found no dependencies to audit that were installed from ' +                      'a supported registry')    }     const invalid = this.invalid.sort(sortAlphabetically)    const missing = this.missing.sort(sortAlphabetically)     const hasNoInvalidOrMissing = invalid.length === 0 && missing.length === 0     if (!hasNoInvalidOrMissing) {      process.exitCode = 1    }     if (this.npm.config.get('json')) {      const result = { invalid, missing }      if (this.npm.config.get('include-attestations')) {        result.verified = this.verified      }      output.buffer(result)      return    }    const end = process.hrtime.bigint()    const elapsed = end - start     const auditedPlural = this.auditedWithKeysCount > 1 ? 's' : ''    const timing = `audited ${this.auditedWithKeysCount} package${auditedPlural} in ` +      `${Math.floor(Number(elapsed) / 1e9)}s`    output.standard(timing)    output.standard()     const verifiedBold = this.npm.chalk.bold('verified')    if (this.verifiedSignatureCount) {      if (this.verifiedSignatureCount === 1) {        output.standard(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`)      } else {        output.standard(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`)      }      output.standard()    }     if (this.verifiedAttestationCount) {      if (this.verifiedAttestationCount === 1) {        output.standard(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`)      } else {        output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`)      }      if (!this.npm.config.get('include-attestations')) {        output.standard('(use --json --include-attestations to view attestation details)')      }      output.standard()    }     if (missing.length) {      const missingClr = this.npm.chalk.redBright('missing')      if (missing.length === 1) {        output.standard(`1 package has a ${missingClr} registry signature but the registry is providing signing keys:`)      } else {        output.standard(`${missing.length} packages have ${missingClr} registry signatures but the registry is providing signing keys:`)      }      output.standard()      missing.map(m =>        output.standard(`${this.npm.chalk.red(`${m.name}@${m.version}`)} (${m.registry})`)      )    }     if (invalid.length) {      if (missing.length) {        output.standard()      }      const invalidClr = this.npm.chalk.redBright('invalid')      // We can have either invalid signatures or invalid provenance      const invalidSignatures = this.invalid.filter(i => i.code === 'EINTEGRITYSIGNATURE')      if (invalidSignatures.length) {        if (invalidSignatures.length === 1) {          output.standard(`1 package has an ${invalidClr} registry signature:`)        } else {          output.standard(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`)        }        output.standard()        invalidSignatures.map(i =>          output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)        )        output.standard()      }       const invalidAttestations = this.invalid.filter(i => i.code === 'EATTESTATIONVERIFY')      if (invalidAttestations.length) {        if (invalidAttestations.length === 1) {          output.standard(`1 package has an ${invalidClr} attestation:`)        } else {          output.standard(`${invalidAttestations.length} packages have ${invalidClr} attestations:`)        }        output.standard()        invalidAttestations.map(i =>          output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)        )        output.standard()      }       if (invalid.length === 1) {        output.standard(`Someone might have tampered with this package since it was published on the registry!`)      } else {        output.standard(`Someone might have tampered with these packages since they were published on the registry!`)      }      output.standard()    }  }   getEdgesOut (nodes, filterSet) {    const edges = new Set()    const registries = new Set()    for (const node of nodes) {      for (const edge of node.edgesOut.values()) {        const filteredOut =          edge.from            && filterSet            && filterSet.size > 0            && !filterSet.has(edge.from.target)         if (!filteredOut) {          const spec = this.getEdgeSpec(edge)          if (spec) {            // Prefetch and cache public keys from used registries            registries.add(this.getSpecRegistry(spec))          }          edges.add(edge)        }      }    }    return { edges, registries }  }   async setKeys ({ registry, tuf }) {    const { host, pathname } = new URL(registry)    // Strip any trailing slashes from pathname    const regKey = `${host}${pathname.replace(/\/$/, '')}/keys.json`    let keys = await tuf.getTarget(regKey)      .then((target) => JSON.parse(target))      .then(({ keys: ks }) => ks.map((key) => ({        ...key,        keyid: key.keyId,        pemkey: `-----BEGIN PUBLIC KEY-----\n${key.publicKey.rawBytes}\n-----END PUBLIC KEY-----`,        expires: key.publicKey.validFor.end || null,      }))).catch(err => {        if (err.code === 'TUF_FIND_TARGET_ERROR') {          return null        } else {          throw err        }      })     // If keys not found in Sigstore TUF repo, fall back to registry keys API    if (!keys) {      log.warn(`Fetching verification keys using TUF failed.  Fetching directly from ${registry}.`)      keys = await npmFetch.json('/-/npm/v1/keys', {        ...this.npm.flatOptions,        registry,      }).then(({ keys: ks }) => ks.map((key) => ({        ...key,        pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`,      }))).catch(err => {        if (err.code === 'E404' || err.code === 'E400') {          return null        } else {          throw err        }      })    }     if (keys) {      this.keys.set(registry, keys)    }  }   getEdgeType (edge) {    return edge.optional ? 'optionalDependencies'      : edge.peer ? 'peerDependencies'      : edge.dev ? 'devDependencies'      : 'dependencies'  }   getEdgeSpec (edge) {    let name = edge.name    try {      name = npa(edge.spec).subSpec.name    } catch {      // leave it as edge.name    }    try {      return npa(`${name}@${edge.spec}`)    } catch {      // Skip packages with invalid spec    }  }   buildRegistryConfig (registry) {    const keys = this.keys.get(registry) || []    const parsedRegistry = new URL(registry)    const regKey = `//${parsedRegistry.host}${parsedRegistry.pathname}`    return {      [`${regKey}:_keys`]: keys,    }  }   getSpecRegistry (spec) {    return npmFetch.pickRegistry(spec, this.npm.flatOptions)  }   getValidPackageInfo (edge) {    const type = this.getEdgeType(edge)    // Skip potentially optional packages that are not on disk, as these could    // be omitted during install    if (edge.error === 'MISSING' && type !== 'dependencies') {      return    }     const spec = this.getEdgeSpec(edge)    // Skip invalid version requirements    if (!spec) {      return    }    const node = edge.to || edge    const { version } = node.package || {}     if (node.isWorkspace || // Skip local workspaces packages        !version || // Skip packages that don't have an installed version, e.g. optional dependencies        !spec.registry) { // Skip if not from registry, e.g. git package      return    }     for (const omitType of this.npm.config.get('omit')) {      if (node[omitType]) {        return      }    }     return {      name: spec.name,      version,      type,      location: node.location,      registry: this.getSpecRegistry(spec),    }  }   async verifySignatures (name, version, registry) {    const {      _integrity: integrity,      _signatures,      _attestations,      _attestationBundles,      _resolved: resolved,    } = await pacote.manifest(`${name}@${version}`, {      verifySignatures: true,      verifyAttestations: true,      ...this.buildRegistryConfig(registry),      ...this.npm.flatOptions,      // These versions are already installed, so the before/min-release-age      // filter doesn't apply here. It was already applied at install time, and      // leaving it set makes pacote throw ETARGET for any version newer than      // the cutoff. See npm/cli#9277.      before: null,    })    const signatures = _signatures || []    const result = {      integrity,      signatures,      attestations: _attestations,      attestationBundles: _attestationBundles,      resolved,    }    return result  }   async getVerifiedInfo (edge) {    const info = this.getValidPackageInfo(edge)    if (!info) {      return    }    const { name, version, location, registry, type } = info    if (this.checkedPackages.has(location)) {      // we already did or are doing this one      return    }    this.checkedPackages.add(location)     // We only "audit" or verify the signature, or the presence of it, on packages whose registry returns signing keys    const keys = this.keys.get(registry) || []    if (keys.length) {      this.auditedWithKeysCount += 1    }     try {      const { integrity, signatures, attestations, attestationBundles, resolved } =        await this.verifySignatures(name, version, registry)       // Currently we only care about missing signatures on registries that provide a public key      // We could make this configurable in the future with a strict/paranoid mode      if (signatures.length) {        this.verifiedSignatureCount += 1      } else if (keys.length) {        this.missing.push({          integrity,          location,          name,          registry,          resolved,          version,        })      }       // 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.      if (attestations) {        this.verifiedAttestationCount += 1        if (this.npm.config.get('include-attestations')) {          this.verified.push({            name,            version,            location,            registry,            attestations,            attestationBundles,          })        }      }    } catch (e) {      if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') {        this.invalid.push({          code: e.code,          message: e.message,          integrity: e.integrity,          keyid: e.keyid,          location,          name,          registry,          resolved: e.resolved,          signature: e.signature,          predicateType: e.predicateType,          type,          version,        })      } else {        throw e      }    }  }} module.exports = VerifySignatures