registry.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. const Fetcher = require('./fetcher.js')
  2. const RemoteFetcher = require('./remote.js')
  3. const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
  4. const pacoteVersion = require('../package.json').version
  5. const removeTrailingSlashes = require('./util/trailing-slashes.js')
  6. const rpj = require('read-package-json-fast')
  7. const pickManifest = require('npm-pick-manifest')
  8. const ssri = require('ssri')
  9. const crypto = require('crypto')
  10. const npa = require('npm-package-arg')
  11. const { sigstore } = require('sigstore')
  12. // Corgis are cute. 🐕🐶
  13. const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
  14. const fullDoc = 'application/json'
  15. const fetch = require('npm-registry-fetch')
  16. const _headers = Symbol('_headers')
  17. class RegistryFetcher extends Fetcher {
  18. constructor (spec, opts) {
  19. super(spec, opts)
  20. // you usually don't want to fetch the same packument multiple times in
  21. // the span of a given script or command, no matter how many pacote calls
  22. // are made, so this lets us avoid doing that. It's only relevant for
  23. // registry fetchers, because other types simulate their packument from
  24. // the manifest, which they memoize on this.package, so it's very cheap
  25. // already.
  26. this.packumentCache = this.opts.packumentCache || null
  27. this.registry = fetch.pickRegistry(spec, opts)
  28. this.packumentUrl = removeTrailingSlashes(this.registry) + '/' +
  29. this.spec.escapedName
  30. const parsed = new URL(this.registry)
  31. const regKey = `//${parsed.host}${parsed.pathname}`
  32. // unlike the nerf-darted auth keys, this one does *not* allow a mismatch
  33. // of trailing slashes. It must match exactly.
  34. if (this.opts[`${regKey}:_keys`]) {
  35. this.registryKeys = this.opts[`${regKey}:_keys`]
  36. }
  37. // XXX pacote <=9 has some logic to ignore opts.resolved if
  38. // the resolved URL doesn't go to the same registry.
  39. // Consider reproducing that here, to throw away this.resolved
  40. // in that case.
  41. }
  42. async resolve () {
  43. // fetching the manifest sets resolved and (if present) integrity
  44. await this.manifest()
  45. if (!this.resolved) {
  46. throw Object.assign(
  47. new Error('Invalid package manifest: no `dist.tarball` field'),
  48. { package: this.spec.toString() }
  49. )
  50. }
  51. return this.resolved
  52. }
  53. [_headers] () {
  54. return {
  55. // npm will override UA, but ensure that we always send *something*
  56. 'user-agent': this.opts.userAgent ||
  57. `pacote/${pacoteVersion} node/${process.version}`,
  58. ...(this.opts.headers || {}),
  59. 'pacote-version': pacoteVersion,
  60. 'pacote-req-type': 'packument',
  61. 'pacote-pkg-id': `registry:${this.spec.name}`,
  62. accept: this.fullMetadata ? fullDoc : corgiDoc,
  63. }
  64. }
  65. async packument () {
  66. // note this might be either an in-flight promise for a request,
  67. // or the actual packument, but we never want to make more than
  68. // one request at a time for the same thing regardless.
  69. if (this.packumentCache && this.packumentCache.has(this.packumentUrl)) {
  70. return this.packumentCache.get(this.packumentUrl)
  71. }
  72. // npm-registry-fetch the packument
  73. // set the appropriate header for corgis if fullMetadata isn't set
  74. // return the res.json() promise
  75. try {
  76. const res = await fetch(this.packumentUrl, {
  77. ...this.opts,
  78. headers: this[_headers](),
  79. spec: this.spec,
  80. // never check integrity for packuments themselves
  81. integrity: null,
  82. })
  83. const packument = await res.json()
  84. packument._contentLength = +res.headers.get('content-length')
  85. if (this.packumentCache) {
  86. this.packumentCache.set(this.packumentUrl, packument)
  87. }
  88. return packument
  89. } catch (err) {
  90. if (this.packumentCache) {
  91. this.packumentCache.delete(this.packumentUrl)
  92. }
  93. if (err.code !== 'E404' || this.fullMetadata) {
  94. throw err
  95. }
  96. // possible that corgis are not supported by this registry
  97. this.fullMetadata = true
  98. return this.packument()
  99. }
  100. }
  101. async manifest () {
  102. if (this.package) {
  103. return this.package
  104. }
  105. const packument = await this.packument()
  106. let mani = await pickManifest(packument, this.spec.fetchSpec, {
  107. ...this.opts,
  108. defaultTag: this.defaultTag,
  109. before: this.before,
  110. })
  111. mani = rpj.normalize(mani)
  112. /* XXX add ETARGET and E403 revalidation of cached packuments here */
  113. // add _resolved and _integrity from dist object
  114. const { dist } = mani
  115. if (dist) {
  116. this.resolved = mani._resolved = dist.tarball
  117. mani._from = this.from
  118. const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
  119. : dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', { ...this.opts })
  120. : null
  121. if (distIntegrity) {
  122. if (this.integrity && !this.integrity.match(distIntegrity)) {
  123. // only bork if they have algos in common.
  124. // otherwise we end up breaking if we have saved a sha512
  125. // previously for the tarball, but the manifest only
  126. // provides a sha1, which is possible for older publishes.
  127. // Otherwise, this is almost certainly a case of holding it
  128. // wrong, and will result in weird or insecure behavior
  129. // later on when building package tree.
  130. for (const algo of Object.keys(this.integrity)) {
  131. if (distIntegrity[algo]) {
  132. throw Object.assign(new Error(
  133. `Integrity checksum failed when using ${algo}: ` +
  134. `wanted ${this.integrity} but got ${distIntegrity}.`
  135. ), { code: 'EINTEGRITY' })
  136. }
  137. }
  138. }
  139. // made it this far, the integrity is worthwhile. accept it.
  140. // the setter here will take care of merging it into what we already
  141. // had.
  142. this.integrity = distIntegrity
  143. }
  144. }
  145. if (this.integrity) {
  146. mani._integrity = String(this.integrity)
  147. if (dist.signatures) {
  148. if (this.opts.verifySignatures) {
  149. // validate and throw on error, then set _signatures
  150. const message = `${mani._id}:${mani._integrity}`
  151. for (const signature of dist.signatures) {
  152. const publicKey = this.registryKeys &&
  153. this.registryKeys.filter(key => (key.keyid === signature.keyid))[0]
  154. if (!publicKey) {
  155. throw Object.assign(new Error(
  156. `${mani._id} has a registry signature with keyid: ${signature.keyid} ` +
  157. 'but no corresponding public key can be found'
  158. ), { code: 'EMISSINGSIGNATUREKEY' })
  159. }
  160. const validPublicKey =
  161. !publicKey.expires || (Date.parse(publicKey.expires) > Date.now())
  162. if (!validPublicKey) {
  163. throw Object.assign(new Error(
  164. `${mani._id} has a registry signature with keyid: ${signature.keyid} ` +
  165. `but the corresponding public key has expired ${publicKey.expires}`
  166. ), { code: 'EEXPIREDSIGNATUREKEY' })
  167. }
  168. const verifier = crypto.createVerify('SHA256')
  169. verifier.write(message)
  170. verifier.end()
  171. const valid = verifier.verify(
  172. publicKey.pemkey,
  173. signature.sig,
  174. 'base64'
  175. )
  176. if (!valid) {
  177. throw Object.assign(new Error(
  178. `${mani._id} has an invalid registry signature with ` +
  179. `keyid: ${publicKey.keyid} and signature: ${signature.sig}`
  180. ), {
  181. code: 'EINTEGRITYSIGNATURE',
  182. keyid: publicKey.keyid,
  183. signature: signature.sig,
  184. resolved: mani._resolved,
  185. integrity: mani._integrity,
  186. })
  187. }
  188. }
  189. mani._signatures = dist.signatures
  190. } else {
  191. mani._signatures = dist.signatures
  192. }
  193. }
  194. if (dist.attestations) {
  195. if (this.opts.verifyAttestations) {
  196. // Always fetch attestations from the current registry host
  197. const attestationsPath = new URL(dist.attestations.url).pathname
  198. const attestationsUrl = removeTrailingSlashes(this.registry) + attestationsPath
  199. const res = await fetch(attestationsUrl, {
  200. ...this.opts,
  201. // disable integrity check for attestations json payload, we check the
  202. // integrity in the verification steps below
  203. integrity: null,
  204. })
  205. const { attestations } = await res.json()
  206. const bundles = attestations.map(({ predicateType, bundle }) => {
  207. const statement = JSON.parse(
  208. Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString('utf8')
  209. )
  210. const keyid = bundle.dsseEnvelope.signatures[0].keyid
  211. const signature = bundle.dsseEnvelope.signatures[0].sig
  212. return {
  213. predicateType,
  214. bundle,
  215. statement,
  216. keyid,
  217. signature,
  218. }
  219. })
  220. const attestationKeyIds = bundles.map((b) => b.keyid).filter((k) => !!k)
  221. const attestationRegistryKeys = (this.registryKeys || [])
  222. .filter(key => attestationKeyIds.includes(key.keyid))
  223. if (!attestationRegistryKeys.length) {
  224. throw Object.assign(new Error(
  225. `${mani._id} has attestations but no corresponding public key(s) can be found`
  226. ), { code: 'EMISSINGSIGNATUREKEY' })
  227. }
  228. for (const { predicateType, bundle, keyid, signature, statement } of bundles) {
  229. const publicKey = attestationRegistryKeys.find(key => key.keyid === keyid)
  230. // Publish attestations have a keyid set and a valid public key must be found
  231. if (keyid) {
  232. if (!publicKey) {
  233. throw Object.assign(new Error(
  234. `${mani._id} has attestations with keyid: ${keyid} ` +
  235. 'but no corresponding public key can be found'
  236. ), { code: 'EMISSINGSIGNATUREKEY' })
  237. }
  238. const validPublicKey =
  239. !publicKey.expires || (Date.parse(publicKey.expires) > Date.now())
  240. if (!validPublicKey) {
  241. throw Object.assign(new Error(
  242. `${mani._id} has attestations with keyid: ${keyid} ` +
  243. `but the corresponding public key has expired ${publicKey.expires}`
  244. ), { code: 'EEXPIREDSIGNATUREKEY' })
  245. }
  246. }
  247. const subject = {
  248. name: statement.subject[0].name,
  249. sha512: statement.subject[0].digest.sha512,
  250. }
  251. // Only type 'version' can be turned into a PURL
  252. const purl = this.spec.type === 'version' ? npa.toPurl(this.spec) : this.spec
  253. // Verify the statement subject matches the package, version
  254. if (subject.name !== purl) {
  255. throw Object.assign(new Error(
  256. `${mani._id} package name and version (PURL): ${purl} ` +
  257. `doesn't match what was signed: ${subject.name}`
  258. ), { code: 'EATTESTATIONSUBJECT' })
  259. }
  260. // Verify the statement subject matches the tarball integrity
  261. const integrityHexDigest = ssri.parse(this.integrity).hexDigest()
  262. if (subject.sha512 !== integrityHexDigest) {
  263. throw Object.assign(new Error(
  264. `${mani._id} package integrity (hex digest): ` +
  265. `${integrityHexDigest} ` +
  266. `doesn't match what was signed: ${subject.sha512}`
  267. ), { code: 'EATTESTATIONSUBJECT' })
  268. }
  269. try {
  270. // Provenance attestations are signed with a signing certificate
  271. // (including the key) so we don't need to return a public key.
  272. //
  273. // Publish attestations are signed with a keyid so we need to
  274. // specify a public key from the keys endpoint: `registry-host.tld/-/npm/v1/keys`
  275. const options = { keySelector: publicKey ? () => publicKey.pemkey : undefined }
  276. await sigstore.verify(bundle, null, options)
  277. } catch (e) {
  278. throw Object.assign(new Error(
  279. `${mani._id} failed to verify attestation: ${e.message}`
  280. ), {
  281. code: 'EATTESTATIONVERIFY',
  282. predicateType,
  283. keyid,
  284. signature,
  285. resolved: mani._resolved,
  286. integrity: mani._integrity,
  287. })
  288. }
  289. }
  290. mani._attestations = dist.attestations
  291. } else {
  292. mani._attestations = dist.attestations
  293. }
  294. }
  295. }
  296. this.package = mani
  297. return this.package
  298. }
  299. [_tarballFromResolved] () {
  300. // we use a RemoteFetcher to get the actual tarball stream
  301. return new RemoteFetcher(this.resolved, {
  302. ...this.opts,
  303. resolved: this.resolved,
  304. pkgid: `registry:${this.spec.name}@${this.resolved}`,
  305. })[_tarballFromResolved]()
  306. }
  307. get types () {
  308. return [
  309. 'tag',
  310. 'version',
  311. 'range',
  312. ]
  313. }
  314. }
  315. module.exports = RegistryFetcher