Skip to content

Commit 37cc797

Browse files
authored
fix: retrieve registry keys via TUF (#6418)
1 parent c3638ce commit 37cc797

File tree

7 files changed

+326
-92
lines changed

7 files changed

+326
-92
lines changed

DEPENDENCIES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,13 +569,15 @@ graph LR;
569569
npm-->remark-github;
570570
npm-->remark;
571571
npm-->semver;
572+
npm-->sigstore;
572573
npm-->spawk;
573574
npm-->ssri;
574575
npm-->tap;
575576
npm-->tar;
576577
npm-->text-table;
577578
npm-->tiny-relative-date;
578579
npm-->treeverse;
580+
npm-->tufjs-repo-mock["@tufjs/repo-mock"];
579581
npm-->validate-npm-package-name;
580582
npm-->which;
581583
npm-->write-file-atomic;

lib/commands/audit.js

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const localeCompare = require('@isaacs/string-locale-compare')('en')
44
const npa = require('npm-package-arg')
55
const pacote = require('pacote')
66
const pMap = require('p-map')
7+
const { sigstore } = require('sigstore')
78

89
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
910
const auditError = require('../utils/audit-error.js')
@@ -37,7 +38,12 @@ class VerifySignatures {
3738
throw new Error('found no installed dependencies to audit')
3839
}
3940

40-
await Promise.all([...registries].map(registry => this.setKeys({ registry })))
41+
const tuf = await sigstore.tuf.client({
42+
tufCachePath: this.opts.tufCache,
43+
retry: this.opts.retry,
44+
timeout: this.opts.timeout,
45+
})
46+
await Promise.all([...registries].map(registry => this.setKeys({ registry, tuf })))
4147

4248
const progress = log.newItem('verifying registry signatures', edges.size)
4349
const mapper = async (edge) => {
@@ -187,20 +193,42 @@ class VerifySignatures {
187193
return { edges, registries }
188194
}
189195

190-
async setKeys ({ registry }) {
191-
const keys = await fetch.json('/-/npm/v1/keys', {
192-
...this.npm.flatOptions,
193-
registry,
194-
}).then(({ keys: ks }) => ks.map((key) => ({
195-
...key,
196-
pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`,
197-
}))).catch(err => {
198-
if (err.code === 'E404' || err.code === 'E400') {
199-
return null
200-
} else {
201-
throw err
202-
}
203-
})
196+
async setKeys ({ registry, tuf }) {
197+
const { host, pathname } = new URL(registry)
198+
// Strip any trailing slashes from pathname
199+
const regKey = `${host}${pathname.replace(/\/$/, '')}/keys.json`
200+
let keys = await tuf.getTarget(regKey)
201+
.then((target) => JSON.parse(target))
202+
.then(({ keys: ks }) => ks.map((key) => ({
203+
...key,
204+
keyid: key.keyId,
205+
pemkey: `-----BEGIN PUBLIC KEY-----\n${key.publicKey.rawBytes}\n-----END PUBLIC KEY-----`,
206+
expires: key.publicKey.validFor.end || null,
207+
}))).catch(err => {
208+
if (err.code === 'TUF_FIND_TARGET_ERROR') {
209+
return null
210+
} else {
211+
throw err
212+
}
213+
})
214+
215+
// If keys not found in Sigstore TUF repo, fallback to registry keys API
216+
if (!keys) {
217+
keys = await fetch.json('/-/npm/v1/keys', {
218+
...this.npm.flatOptions,
219+
registry,
220+
}).then(({ keys: ks }) => ks.map((key) => ({
221+
...key,
222+
pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`,
223+
}))).catch(err => {
224+
if (err.code === 'E404' || err.code === 'E400') {
225+
return null
226+
} else {
227+
throw err
228+
}
229+
})
230+
}
231+
204232
if (keys) {
205233
this.keys.set(registry, keys)
206234
}

lib/utils/config/definitions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ define('cache', {
331331
flatten (key, obj, flatOptions) {
332332
flatOptions.cache = join(obj.cache, '_cacache')
333333
flatOptions.npxCache = join(obj.cache, '_npx')
334+
flatOptions.tufCache = join(obj.cache, '_tuf')
334335
},
335336
})
336337

package-lock.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"read-package-json",
6767
"read-package-json-fast",
6868
"semver",
69+
"sigstore",
6970
"ssri",
7071
"tar",
7172
"text-table",
@@ -141,6 +142,7 @@
141142
"read-package-json": "^6.0.3",
142143
"read-package-json-fast": "^3.0.2",
143144
"semver": "^7.5.1",
145+
"sigstore": "^1.5.0",
144146
"ssri": "^10.0.4",
145147
"tar": "^6.1.14",
146148
"text-table": "~0.2.0",
@@ -162,6 +164,7 @@
162164
"@npmcli/mock-registry": "^1.0.0",
163165
"@npmcli/promise-spawn": "^6.0.2",
164166
"@npmcli/template-oss": "4.14.1",
167+
"@tufjs/repo-mock": "^1.3.1",
165168
"licensee": "^10.0.0",
166169
"nock": "^13.3.0",
167170
"npm-packlist": "^7.0.4",
@@ -2642,6 +2645,19 @@
26422645
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
26432646
}
26442647
},
2648+
"node_modules/@tufjs/repo-mock": {
2649+
"version": "1.3.1",
2650+
"resolved": "https://registry.npmjs.org/@tufjs/repo-mock/-/repo-mock-1.3.1.tgz",
2651+
"integrity": "sha512-7IDezQbPGReWD3xmgR2pAfG61BZpvW51XnB87OfuiJOe5mkGnziCTTGITtUC3A6htQr9shkk5qIKrhpoMXBwpQ==",
2652+
"dev": true,
2653+
"dependencies": {
2654+
"@tufjs/models": "1.0.4",
2655+
"nock": "^13.3.1"
2656+
},
2657+
"engines": {
2658+
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
2659+
}
2660+
},
26452661
"node_modules/@types/debug": {
26462662
"version": "4.1.7",
26472663
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"read-package-json": "^6.0.3",
111111
"read-package-json-fast": "^3.0.2",
112112
"semver": "^7.5.1",
113+
"sigstore": "^1.5.0",
113114
"ssri": "^10.0.4",
114115
"tar": "^6.1.14",
115116
"text-table": "~0.2.0",
@@ -178,6 +179,7 @@
178179
"read-package-json",
179180
"read-package-json-fast",
180181
"semver",
182+
"sigstore",
181183
"ssri",
182184
"tar",
183185
"text-table",
@@ -195,6 +197,7 @@
195197
"@npmcli/mock-registry": "^1.0.0",
196198
"@npmcli/promise-spawn": "^6.0.2",
197199
"@npmcli/template-oss": "4.14.1",
200+
"@tufjs/repo-mock": "^1.3.1",
198201
"licensee": "^10.0.0",
199202
"nock": "^13.3.0",
200203
"npm-packlist": "^7.0.4",

tap-snapshots/test/lib/commands/audit.js.test.cjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,20 @@ audited 1 package in xxx
175175
176176
`
177177

178+
exports[`test/lib/commands/audit.js TAP audit signatures third-party registry with sub-path (trailing slash) > must match snapshot 1`] = `
179+
audited 1 package in xxx
180+
181+
1 package has a verified registry signature
182+
183+
`
184+
185+
exports[`test/lib/commands/audit.js TAP audit signatures third-party registry with sub-path > must match snapshot 1`] = `
186+
audited 1 package in xxx
187+
188+
1 package has a verified registry signature
189+
190+
`
191+
178192
exports[`test/lib/commands/audit.js TAP audit signatures with both invalid and missing signatures > must match snapshot 1`] = `
179193
audited 2 packages in xxx
180194
@@ -230,6 +244,13 @@ Someone might have tampered with this package since it was published on the regi
230244
231245
`
232246

247+
exports[`test/lib/commands/audit.js TAP audit signatures with key fallback to legacy API > must match snapshot 1`] = `
248+
audited 1 package in xxx
249+
250+
1 package has a verified registry signature
251+
252+
`
253+
233254
exports[`test/lib/commands/audit.js TAP audit signatures with keys but missing signature > must match snapshot 1`] = `
234255
audited 1 package in xxx
235256

0 commit comments

Comments
 (0)