Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* pass prefix and workspaces to npm-packlist
* add verifySignatures to registry.manifest
  • Loading branch information
wraithgar authored and lukekarrys committed May 19, 2022
1 parent 7b2b77a commit f3b0a24
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 89 deletions.
4 changes: 3 additions & 1 deletion node_modules/pacote/lib/dir.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ class DirFetcher extends Fetcher {
stream.resolved = this.resolved
stream.integrity = this.integrity

const { prefix, workspaces } = this.opts

// run the prepare script, get the list of files, and tar it up
// pipe to the stream, and proxy errors the chain.
this[_prepareDir]()
.then(() => packlist({ path: this.resolved }))
.then(() => packlist({ path: this.resolved, prefix, workspaces }))
.then(files => tar.c(tarCreateOptions(this.package), files)
.on('error', er => stream.emit('error', er)).pipe(stream))
.catch(er => stream.emit('error', er))
Expand Down
191 changes: 114 additions & 77 deletions node_modules/pacote/lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ const npa = require('npm-package-arg')
const rpj = require('read-package-json-fast')
const pickManifest = require('npm-pick-manifest')
const ssri = require('ssri')
const crypto = require('crypto')

// Corgis are cute. 🐕🐶
const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
const fullDoc = 'application/json'

const fetch = require('npm-registry-fetch')

// TODO: memoize reg requests, so we don't even have to check cache

const _headers = Symbol('_headers')
class RegistryFetcher extends Fetcher {
constructor (spec, opts) {
Expand All @@ -39,28 +38,30 @@ class RegistryFetcher extends Fetcher {
this.packumentUrl = removeTrailingSlashes(this.registry) + '/' +
this.spec.escapedName

const parsed = new URL(this.registry)
const regKey = `//${parsed.host}${parsed.pathname}`
// unlike the nerf-darted auth keys, this one does *not* allow a mismatch
// of trailing slashes. It must match exactly.
if (this.opts[`${regKey}:_keys`]) {
this.registryKeys = this.opts[`${regKey}:_keys`]
}

// XXX pacote <=9 has some logic to ignore opts.resolved if
// the resolved URL doesn't go to the same registry.
// Consider reproducing that here, to throw away this.resolved
// in that case.
}

resolve () {
if (this.resolved) {
return Promise.resolve(this.resolved)
}

// fetching the manifest sets resolved and (usually) integrity
return this.manifest().then(() => {
if (this.resolved) {
return this.resolved
}

async resolve () {
// fetching the manifest sets resolved and (if present) integrity
await this.manifest()
if (!this.resolved) {
throw Object.assign(
new Error('Invalid package manifest: no `dist.tarball` field'),
{ package: this.spec.toString() }
)
})
}
return this.resolved
}

[_headers] () {
Expand All @@ -87,91 +88,127 @@ class RegistryFetcher extends Fetcher {
// npm-registry-fetch the packument
// set the appropriate header for corgis if fullMetadata isn't set
// return the res.json() promise
const p = fetch(this.packumentUrl, {
...this.opts,
headers: this[_headers](),
spec: this.spec,
// never check integrity for packuments themselves
integrity: null,
}).then(res => res.json().then(packument => {
try {
const res = await fetch(this.packumentUrl, {
...this.opts,
headers: this[_headers](),
spec: this.spec,
// never check integrity for packuments themselves
integrity: null,
})
const packument = await res.json()
packument._cached = res.headers.has('x-local-cache')
packument._contentLength = +res.headers.get('content-length')
if (this.packumentCache) {
this.packumentCache.set(this.packumentUrl, packument)
}
return packument
})).catch(er => {
} catch (err) {
if (this.packumentCache) {
this.packumentCache.delete(this.packumentUrl)
}
if (er.code === 'E404' && !this.fullMetadata) {
// possible that corgis are not supported by this registry
this.fullMetadata = true
return this.packument()
if (err.code !== 'E404' || this.fullMetadata) {
throw err
}
throw er
})
if (this.packumentCache) {
this.packumentCache.set(this.packumentUrl, p)
// possible that corgis are not supported by this registry
this.fullMetadata = true
return this.packument()
}
return p
}

manifest () {
async manifest () {
if (this.package) {
return Promise.resolve(this.package)
return this.package
}

return this.packument()
.then(packument => pickManifest(packument, this.spec.fetchSpec, {
...this.opts,
defaultTag: this.defaultTag,
before: this.before,
}) /* XXX add ETARGET and E403 revalidation of cached packuments here */)
.then(mani => {
// add _resolved and _integrity from dist object
const { dist } = mani
if (dist) {
this.resolved = mani._resolved = dist.tarball
mani._from = this.from
const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
: dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', { ...this.opts })
: null
if (distIntegrity) {
if (!this.integrity) {
this.integrity = distIntegrity
} else if (!this.integrity.match(distIntegrity)) {
// only bork if they have algos in common.
// otherwise we end up breaking if we have saved a sha512
// previously for the tarball, but the manifest only
// provides a sha1, which is possible for older publishes.
// Otherwise, this is almost certainly a case of holding it
// wrong, and will result in weird or insecure behavior
// later on when building package tree.
for (const algo of Object.keys(this.integrity)) {
if (distIntegrity[algo]) {
throw Object.assign(new Error(
`Integrity checksum failed when using ${algo}: ` +
`wanted ${this.integrity} but got ${distIntegrity}.`
), { code: 'EINTEGRITY' })
}
}
// made it this far, the integrity is worthwhile. accept it.
// the setter here will take care of merging it into what we
// already had.
this.integrity = distIntegrity
const packument = await this.packument()
const mani = await pickManifest(packument, this.spec.fetchSpec, {
...this.opts,
defaultTag: this.defaultTag,
before: this.before,
})
/* XXX add ETARGET and E403 revalidation of cached packuments here */

// add _resolved and _integrity from dist object
const { dist } = mani
if (dist) {
this.resolved = mani._resolved = dist.tarball
mani._from = this.from
const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
: dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', { ...this.opts })
: null
if (distIntegrity) {
if (this.integrity && !this.integrity.match(distIntegrity)) {
// only bork if they have algos in common.
// otherwise we end up breaking if we have saved a sha512
// previously for the tarball, but the manifest only
// provides a sha1, which is possible for older publishes.
// Otherwise, this is almost certainly a case of holding it
// wrong, and will result in weird or insecure behavior
// later on when building package tree.
for (const algo of Object.keys(this.integrity)) {
if (distIntegrity[algo]) {
throw Object.assign(new Error(
`Integrity checksum failed when using ${algo}: ` +
`wanted ${this.integrity} but got ${distIntegrity}.`
), { code: 'EINTEGRITY' })
}
}
}
if (this.integrity) {
mani._integrity = String(this.integrity)
if (dist.signatures) {
// made it this far, the integrity is worthwhile. accept it.
// the setter here will take care of merging it into what we already
// had.
this.integrity = distIntegrity
}
}
if (this.integrity) {
mani._integrity = String(this.integrity)
if (dist.signatures) {
if (this.opts.verifySignatures) {
if (this.registryKeys) {
// validate and throw on error, then set _signatures
const message = `${mani._id}:${mani._integrity}`
for (const signature of dist.signatures) {
const publicKey = this.registryKeys.filter(key => (key.keyid === signature.keyid))[0]
if (!publicKey) {
throw Object.assign(new Error(
`${mani._id} has a signature with keyid: ${signature.keyid} ` +
'but no corresponding public key can be found.'
), { code: 'EMISSINGSIGNATUREKEY' })
}
const validPublicKey =
!publicKey.expires || (Date.parse(publicKey.expires) > Date.now())
if (!validPublicKey) {
throw Object.assign(new Error(
`${mani._id} has a signature with keyid: ${signature.keyid} ` +
`but the corresponding public key has expired ${publicKey.expires}`
), { code: 'EEXPIREDSIGNATUREKEY' })
}
const verifier = crypto.createVerify('SHA256')
verifier.write(message)
verifier.end()
const valid = verifier.verify(
publicKey.pemkey,
signature.sig,
'base64'
)
if (!valid) {
throw Object.assign(new Error(
'Integrity checksum signature failed: ' +
`key ${publicKey.keyid} signature ${signature.sig}`
), { code: 'EINTEGRITYSIGNATURE' })
}
}
mani._signatures = dist.signatures
}
// if no keys, don't set _signatures
} else {
mani._signatures = dist.signatures
}
this.package = rpj.normalize(mani)
return this.package
})
}
}
this.package = rpj.normalize(mani)
return this.package
}

[_tarballFromResolved] () {
Expand Down
6 changes: 3 additions & 3 deletions node_modules/pacote/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pacote",
"version": "13.3.0",
"version": "13.4.1",
"description": "JavaScript package downloader",
"author": "GitHub Inc.",
"bin": {
Expand All @@ -26,7 +26,7 @@
},
"devDependencies": {
"@npmcli/eslint-config": "^3.0.1",
"@npmcli/template-oss": "3.4.3",
"@npmcli/template-oss": "3.5.0",
"hosted-git-info": "^5.0.0",
"mutate-fs": "^2.1.1",
"nock": "^13.2.4",
Expand Down Expand Up @@ -74,7 +74,7 @@
},
"templateOSS": {
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
"version": "3.4.3",
"version": "3.5.0",
"windowsCI": false
}
}
14 changes: 7 additions & 7 deletions package-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
"npm-user-validate": "^1.0.1",
"npmlog": "^6.0.2",
"opener": "^1.5.2",
"pacote": "^13.3.0",
"pacote": "^13.4.1",
"parse-conflict-json": "^2.0.2",
"proc-log": "^2.0.1",
"qrcode-terminal": "^0.12.0",
Expand Down Expand Up @@ -5547,9 +5547,9 @@
}
},
"node_modules/pacote": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/pacote/-/pacote-13.3.0.tgz",
"integrity": "sha512-auhJAUlfC2TALo6I0s1vFoPvVFgWGx+uz/PnIojTTgkGwlK3Np8sGJ0ghfFhiuzJXTZoTycMLk8uLskdntPbDw==",
"version": "13.4.1",
"resolved": "https://registry.npmjs.org/pacote/-/pacote-13.4.1.tgz",
"integrity": "sha512-FqlSWlD8n+ejCE17GF/lf0yasztMGFl4UFzYQk5njaK/qPPWfVDWnfQwqmqeXObWLSmIBew+O+CFD24vxkVDjg==",
"inBundle": true,
"dependencies": {
"@npmcli/git": "^3.0.0",
Expand Down Expand Up @@ -13877,9 +13877,9 @@
}
},
"pacote": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/pacote/-/pacote-13.3.0.tgz",
"integrity": "sha512-auhJAUlfC2TALo6I0s1vFoPvVFgWGx+uz/PnIojTTgkGwlK3Np8sGJ0ghfFhiuzJXTZoTycMLk8uLskdntPbDw==",
"version": "13.4.1",
"resolved": "https://registry.npmjs.org/pacote/-/pacote-13.4.1.tgz",
"integrity": "sha512-FqlSWlD8n+ejCE17GF/lf0yasztMGFl4UFzYQk5njaK/qPPWfVDWnfQwqmqeXObWLSmIBew+O+CFD24vxkVDjg==",
"requires": {
"@npmcli/git": "^3.0.0",
"@npmcli/installed-package-contents": "^1.0.7",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"npm-user-validate": "^1.0.1",
"npmlog": "^6.0.2",
"opener": "^1.5.2",
"pacote": "^13.3.0",
"pacote": "^13.4.1",
"parse-conflict-json": "^2.0.2",
"proc-log": "^2.0.1",
"qrcode-terminal": "^0.12.0",
Expand Down

0 comments on commit f3b0a24

Please sign in to comment.