diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 192c9cdf7903c..aded27c1ee2f4 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -569,6 +569,7 @@ graph LR; npm-->remark-github; npm-->remark; npm-->semver; + npm-->sigstore; npm-->spawk; npm-->ssri; npm-->tap; @@ -576,6 +577,7 @@ graph LR; npm-->text-table; npm-->tiny-relative-date; npm-->treeverse; + npm-->tufjs-repo-mock["@tufjs/repo-mock"]; npm-->validate-npm-package-name; npm-->which; npm-->write-file-atomic; diff --git a/lib/commands/audit.js b/lib/commands/audit.js index 7b75ecbf2e024..dfdb783fc3023 100644 --- a/lib/commands/audit.js +++ b/lib/commands/audit.js @@ -4,6 +4,7 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const npa = require('npm-package-arg') const pacote = require('pacote') const pMap = require('p-map') +const { sigstore } = require('sigstore') const ArboristWorkspaceCmd = require('../arborist-cmd.js') const auditError = require('../utils/audit-error.js') @@ -37,7 +38,12 @@ class VerifySignatures { throw new Error('found no installed dependencies to audit') } - await Promise.all([...registries].map(registry => this.setKeys({ registry }))) + const tuf = await sigstore.tuf.client({ + tufCachePath: this.opts.tufCache, + retry: this.opts.retry, + timeout: this.opts.timeout, + }) + await Promise.all([...registries].map(registry => this.setKeys({ registry, tuf }))) const progress = log.newItem('verifying registry signatures', edges.size) const mapper = async (edge) => { @@ -187,20 +193,42 @@ class VerifySignatures { return { edges, registries } } - async setKeys ({ registry }) { - const keys = await fetch.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 - } - }) + 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, fallback to registry keys API + if (!keys) { + keys = await fetch.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) } diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 78c341eabeffa..82e0d5b9223c2 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -331,6 +331,7 @@ define('cache', { flatten (key, obj, flatOptions) { flatOptions.cache = join(obj.cache, '_cacache') flatOptions.npxCache = join(obj.cache, '_npx') + flatOptions.tufCache = join(obj.cache, '_tuf') }, }) diff --git a/package-lock.json b/package-lock.json index ad07549f25920..b9bf41ff7a765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "read-package-json", "read-package-json-fast", "semver", + "sigstore", "ssri", "tar", "text-table", @@ -141,6 +142,7 @@ "read-package-json": "^6.0.3", "read-package-json-fast": "^3.0.2", "semver": "^7.5.1", + "sigstore": "^1.5.0", "ssri": "^10.0.4", "tar": "^6.1.14", "text-table": "~0.2.0", @@ -162,6 +164,7 @@ "@npmcli/mock-registry": "^1.0.0", "@npmcli/promise-spawn": "^6.0.2", "@npmcli/template-oss": "4.14.1", + "@tufjs/repo-mock": "^1.3.1", "licensee": "^10.0.0", "nock": "^13.3.0", "npm-packlist": "^7.0.4", @@ -2642,6 +2645,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@tufjs/repo-mock": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@tufjs/repo-mock/-/repo-mock-1.3.1.tgz", + "integrity": "sha512-7IDezQbPGReWD3xmgR2pAfG61BZpvW51XnB87OfuiJOe5mkGnziCTTGITtUC3A6htQr9shkk5qIKrhpoMXBwpQ==", + "dev": true, + "dependencies": { + "@tufjs/models": "1.0.4", + "nock": "^13.3.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", diff --git a/package.json b/package.json index 123ecc8fd54c0..ba80a917419bd 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "read-package-json": "^6.0.3", "read-package-json-fast": "^3.0.2", "semver": "^7.5.1", + "sigstore": "^1.5.0", "ssri": "^10.0.4", "tar": "^6.1.14", "text-table": "~0.2.0", @@ -178,6 +179,7 @@ "read-package-json", "read-package-json-fast", "semver", + "sigstore", "ssri", "tar", "text-table", @@ -195,6 +197,7 @@ "@npmcli/mock-registry": "^1.0.0", "@npmcli/promise-spawn": "^6.0.2", "@npmcli/template-oss": "4.14.1", + "@tufjs/repo-mock": "^1.3.1", "licensee": "^10.0.0", "nock": "^13.3.0", "npm-packlist": "^7.0.4", diff --git a/tap-snapshots/test/lib/commands/audit.js.test.cjs b/tap-snapshots/test/lib/commands/audit.js.test.cjs index 4fec8f86c5baa..7611191688268 100644 --- a/tap-snapshots/test/lib/commands/audit.js.test.cjs +++ b/tap-snapshots/test/lib/commands/audit.js.test.cjs @@ -175,6 +175,20 @@ audited 1 package in xxx ` +exports[`test/lib/commands/audit.js TAP audit signatures third-party registry with sub-path (trailing slash) > must match snapshot 1`] = ` +audited 1 package in xxx + +1 package has a verified registry signature + +` + +exports[`test/lib/commands/audit.js TAP audit signatures third-party registry with sub-path > must match snapshot 1`] = ` +audited 1 package in xxx + +1 package has a verified registry signature + +` + exports[`test/lib/commands/audit.js TAP audit signatures with both invalid and missing signatures > must match snapshot 1`] = ` audited 2 packages in xxx @@ -230,6 +244,13 @@ Someone might have tampered with this package since it was published on the regi ` +exports[`test/lib/commands/audit.js TAP audit signatures with key fallback to legacy API > must match snapshot 1`] = ` +audited 1 package in xxx + +1 package has a verified registry signature + +` + exports[`test/lib/commands/audit.js TAP audit signatures with keys but missing signature > must match snapshot 1`] = ` audited 1 package in xxx diff --git a/test/lib/commands/audit.js b/test/lib/commands/audit.js index 5c82fa14de32c..911484fe479ce 100644 --- a/test/lib/commands/audit.js +++ b/test/lib/commands/audit.js @@ -3,6 +3,7 @@ const zlib = require('zlib') const path = require('path') const t = require('tap') +const { default: tufmock } = require('@tufjs/repo-mock') const { load: loadMockNpm } = require('../../fixtures/mock-npm') const MockRegistry = require('@npmcli/mock-registry') @@ -247,28 +248,69 @@ t.test('audit signatures', async t => { }], } - const MISMATCHING_REGISTRY_KEYS = { + const TUF_VALID_REGISTRY_KEYS = { keys: [{ - expires: null, - keyid: 'SHA256:2l3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', - keytype: 'ecdsa-sha2-nistp256', - scheme: 'ecdsa-sha2-nistp256', - key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + + keyId: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', + keyUsage: 'npm:signatures', + publicKey: { + rawBytes: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', + keyDetails: 'PKIX_ECDSA_P256_SHA_256', + validFor: { + start: '1999-01-01T00:00:00.000Z', + }, + }, }], } - const EXPIRED_REGISTRY_KEYS = { + const TUF_MISMATCHING_REGISTRY_KEYS = { keys: [{ - expires: '2021-01-11T15:45:42.144Z', - keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', - keytype: 'ecdsa-sha2-nistp256', - scheme: 'ecdsa-sha2-nistp256', - key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + + keyId: 'SHA256:2l3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', + keyUsage: 'npm:signatures', + publicKey: { + rawBytes: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + + 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', + keyDetails: 'PKIX_ECDSA_P256_SHA_256', + validFor: { + start: '1999-01-01T00:00:00.000Z', + }, + }, + }], + } + + const TUF_EXPIRED_REGISTRY_KEYS = { + keys: [{ + keyId: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', + keyUsage: 'npm:signatures', + publicKey: { + rawBytes: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', + keyDetails: 'PKIX_ECDSA_P256_SHA_256', + validFor: { + start: '1999-01-01T00:00:00.000Z', + end: '2021-01-11T15:45:42.144Z', + }, + }, }], } + const TUF_VALID_KEYS_TARGET = { + name: 'registry.npmjs.org/keys.json', + content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), + } + + const TUF_MISMATCHING_KEYS_TARGET = { + name: 'registry.npmjs.org/keys.json', + content: JSON.stringify(TUF_MISMATCHING_REGISTRY_KEYS), + } + + const TUF_EXPIRED_KEYS_TARGET = { + name: 'registry.npmjs.org/keys.json', + content: JSON.stringify(TUF_EXPIRED_REGISTRY_KEYS), + } + + const TUF_TARGET_NOT_FOUND = [] + const installWithValidSigs = { 'package.json': JSON.stringify({ name: 'test-dep', @@ -882,13 +924,22 @@ t.test('audit signatures', async t => { await registry.package({ manifest }) } + function mockTUF ({ target, npm }) { + const opts = { + baseURL: 'https://tuf-repo-cdn.sigstore.dev', + metadataPathPrefix: '', + cachePath: path.join(npm.cache, '_tuf'), + } + return tufmock(target, opts) + } + t.test('with valid signatures', async t => { const { npm, joinedOutput } = await loadMockNpm(t, { prefixDir: installWithValidSigs, }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -921,6 +972,22 @@ t.test('audit signatures', async t => { }], }) await registry.package({ manifest }) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) + + await npm.exec('audit', ['signatures']) + + t.notOk(process.exitCode, 'should exit successfully') + t.match(joinedOutput(), /audited 1 package/) + t.matchSnapshot(joinedOutput()) + }) + + t.test('with key fallback to legacy API', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir: installWithValidSigs, + }) + const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) + await manifestWithValidSigs({ registry }) + mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) await npm.exec('audit', ['signatures']) @@ -1027,7 +1094,7 @@ t.test('audit signatures', async t => { }) await registry.package({ manifest: asyncManifest }) await manifestWithInvalidSigs({ registry, name: 'node-fetch', version: '1.6.0' }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1044,7 +1111,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1059,7 +1126,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithInvalidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1076,7 +1143,7 @@ t.test('audit signatures', async t => { const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1094,7 +1161,7 @@ t.test('audit signatures', async t => { const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithInvalidSigs({ registry }) await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1112,7 +1179,7 @@ t.test('audit signatures', async t => { const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithInvalidSigs({ registry, name: 'kms-demo', version: '1.0.0' }) await manifestWithInvalidSigs({ registry, name: 'async', version: '1.1.1' }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1127,7 +1194,7 @@ t.test('audit signatures', async t => { const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithoutSigs({ registry, name: 'kms-demo', version: '1.0.0' }) await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1141,6 +1208,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) + mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) registry.nock.get('/-/npm/v1/keys').reply(404) await t.rejects( @@ -1156,7 +1224,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, EXPIRED_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_EXPIRED_KEYS_TARGET }) await t.rejects( npm.exec('audit', ['signatures']), @@ -1171,7 +1239,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, MISMATCHING_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_MISMATCHING_KEYS_TARGET }) await t.rejects( npm.exec('audit', ['signatures']), @@ -1186,7 +1254,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithoutSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1204,7 +1272,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithoutSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1225,7 +1293,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1243,7 +1311,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithInvalidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1261,7 +1329,7 @@ t.test('audit signatures', async t => { const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithInvalidSigs({ registry }) await manifestWithoutSigs({ registry, name: 'async', version: '1.1.1' }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1278,7 +1346,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1309,6 +1377,7 @@ t.test('audit signatures', async t => { }], }) await registry.package({ manifest }) + mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) registry.nock.get('/-/npm/v1/keys').reply(404) await t.rejects( @@ -1339,6 +1408,7 @@ t.test('audit signatures', async t => { }], }) await registry.package({ manifest }) + mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) registry.nock.get('/-/npm/v1/keys').reply(400) await t.rejects( @@ -1377,17 +1447,11 @@ t.test('audit signatures', async t => { }], }) await registry.package({ manifest }) - registry.nock.get('/-/npm/v1/keys') - .reply(200, { - keys: [{ - expires: null, - keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', - keytype: 'ecdsa-sha2-nistp256', - scheme: 'ecdsa-sha2-nistp256', - key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + - 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', - }], - }) + mockTUF({ npm, + target: { + name: 'verdaccio-clone.org/keys.json', + content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), + } }) await npm.exec('audit', ['signatures']) @@ -1425,17 +1489,11 @@ t.test('audit signatures', async t => { }], }) await registry.package({ manifest }) - registry.nock.get('/-/npm/v1/keys') - .reply(200, { - keys: [{ - expires: null, - keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', - keytype: 'ecdsa-sha2-nistp256', - scheme: 'ecdsa-sha2-nistp256', - key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + - 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', - }], - }) + mockTUF({ npm, + target: { + name: 'verdaccio-clone.org/keys.json', + content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), + } }) await npm.exec('audit', ['signatures']) @@ -1467,17 +1525,11 @@ t.test('audit signatures', async t => { }], }) await registry.package({ manifest }) - registry.nock.get('/-/npm/v1/keys') - .reply(200, { - keys: [{ - expires: null, - keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', - keytype: 'ecdsa-sha2-nistp256', - scheme: 'ecdsa-sha2-nistp256', - key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+' + - 'IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==', - }], - }) + mockTUF({ npm, + target: { + name: 'verdaccio-clone.org/keys.json', + content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), + } }) await npm.exec('audit', ['signatures']) @@ -1486,6 +1538,94 @@ t.test('audit signatures', async t => { t.matchSnapshot(joinedOutput()) }) + t.test('third-party registry with sub-path', async t => { + const registryUrl = 'https://verdaccio-clone.org/npm' + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir: installWithThirdPartyRegistry, + config: { + scope: '@npmcli', + registry: registryUrl, + }, + }) + const registry = new MockRegistry({ tap: t, registry: registryUrl }) + + const manifest = registry.manifest({ + name: '@npmcli/arborist', + packuments: [{ + version: '1.0.14', + dist: { + tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', + integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + + 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', + signatures: [ + { + keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', + sig: 'MEUCIAvNpR3G0j7WOPUuVMhE0ZdM8PnDNcsoeFD8Iwz9YWIMAiEAn8cicDC2' + + 'Sf9MFQydqTv6S5XYsAh9Af1sig1nApNI11M=', + }, + ], + }, + }], + }) + await registry.package({ manifest }) + + mockTUF({ npm, + target: { + name: 'verdaccio-clone.org/npm/keys.json', + content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), + } }) + + await npm.exec('audit', ['signatures']) + + t.notOk(process.exitCode, 'should exit successfully') + t.match(joinedOutput(), /audited 1 package/) + t.matchSnapshot(joinedOutput()) + }) + + t.test('third-party registry with sub-path (trailing slash)', async t => { + const registryUrl = 'https://verdaccio-clone.org/npm/' + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir: installWithThirdPartyRegistry, + config: { + scope: '@npmcli', + registry: registryUrl, + }, + }) + const registry = new MockRegistry({ tap: t, registry: registryUrl }) + + const manifest = registry.manifest({ + name: '@npmcli/arborist', + packuments: [{ + version: '1.0.14', + dist: { + tarball: 'https://registry.npmjs.org/@npmcli/arborist/-/@npmcli/arborist-1.0.14.tgz', + integrity: 'sha512-caa8hv5rW9VpQKk6tyNRvSaVDySVjo9GkI7Wj/wcsFyxPm3tYrE' + + 'sFyTjSnJH8HCIfEGVQNjqqKXaXLFVp7UBag==', + signatures: [ + { + keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA', + sig: 'MEUCIAvNpR3G0j7WOPUuVMhE0ZdM8PnDNcsoeFD8Iwz9YWIMAiEAn8cicDC2' + + 'Sf9MFQydqTv6S5XYsAh9Af1sig1nApNI11M=', + }, + ], + }, + }], + }) + await registry.package({ manifest }) + + mockTUF({ npm, + target: { + name: 'verdaccio-clone.org/npm/keys.json', + content: JSON.stringify(TUF_VALID_REGISTRY_KEYS), + } }) + + await npm.exec('audit', ['signatures']) + + t.notOk(process.exitCode, 'should exit successfully') + t.match(joinedOutput(), /audited 1 package/) + t.matchSnapshot(joinedOutput()) + }) + t.test('multiple registries with keys and signatures', async t => { const registryUrl = 'https://verdaccio-clone.org' const { npm, joinedOutput } = await loadMockNpm(t, { @@ -1500,7 +1640,7 @@ t.test('audit signatures', async t => { registry: registryUrl, }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) const manifest = thirdPartyRegistry.manifest({ name: '@npmcli/arborist', @@ -1556,11 +1696,36 @@ t.test('audit signatures', async t => { ) }) + t.test('errors when TUF errors', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: installWithMultipleDeps, + mocks: { + sigstore: { + sigstore: { + tuf: { + client: async () => ({ + getTarget: async () => { + throw new Error('error refreshing TUF metadata') + }, + }), + }, + }, + }, + }, + }) + + await t.rejects( + npm.exec('audit', ['signatures']), + /error refreshing TUF metadata/ + ) + }) + t.test('errors when the keys endpoint errors', async t => { const { npm } = await loadMockNpm(t, { prefixDir: installWithMultipleDeps, }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) + mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) registry.nock.get('/-/npm/v1/keys') .reply(500, { error: 'keys broke' }) @@ -1577,7 +1742,7 @@ t.test('audit signatures', async t => { const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithValidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1590,8 +1755,7 @@ t.test('audit signatures', async t => { const { npm } = await loadMockNpm(t, { prefixDir: noInstall, }) - const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await t.rejects( npm.exec('audit', ['signatures']), @@ -1612,8 +1776,7 @@ t.test('audit signatures', async t => { node_modules: {}, }, }) - const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await t.rejects( npm.exec('audit', ['signatures']), @@ -1641,6 +1804,7 @@ t.test('audit signatures', async t => { }, }, }) + mockTUF({ npm, target: TUF_TARGET_NOT_FOUND }) await t.rejects( npm.exec('audit', ['signatures']), @@ -1669,8 +1833,7 @@ t.test('audit signatures', async t => { }, }) - const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await t.rejects( npm.exec('audit', ['signatures']), @@ -1697,7 +1860,7 @@ t.test('audit signatures', async t => { }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') }) await manifestWithInvalidSigs({ registry }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1728,7 +1891,7 @@ t.test('audit signatures', async t => { 'utf8' ) registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1761,7 +1924,7 @@ t.test('audit signatures', async t => { ) registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture1) registry.nock.get('/-/npm/v1/attestations/tuf-js@1.0.0').reply(200, fixture2) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1791,7 +1954,7 @@ t.test('audit signatures', async t => { 'utf8' ) registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1828,7 +1991,7 @@ t.test('audit signatures', async t => { 'utf8' ) registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1865,7 +2028,7 @@ t.test('audit signatures', async t => { ) registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture1) registry.nock.get('/-/npm/v1/attestations/tuf-js@1.0.0').reply(200, fixture2) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1922,7 +2085,7 @@ t.test('audit signatures', async t => { }) await registry.package({ manifest: asyncManifest }) await registry.package({ manifest: lightCycleManifest }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures']) @@ -1975,7 +2138,7 @@ t.test('audit signatures', async t => { }) await registry.package({ manifest: asyncManifest }) await registry.package({ manifest: lightCycleManifest }) - registry.nock.get('/-/npm/v1/keys').reply(200, VALID_REGISTRY_KEYS) + mockTUF({ npm, target: TUF_VALID_KEYS_TARGET }) await npm.exec('audit', ['signatures'])