Skip to content

Commit 79bfd03

Browse files
feelepxyzwraithgar
authored andcommitted
feat: audit signatures verifies attestations
Update `audit signatures` to also verify Sigstore attestations. Additional changes: - Adding error message to json error output as there are a lot of different failure cases with signature verification that would be hard to debug without this - Adding predicateType to json error output for attestations to diffentiate between provenance and publish attestations References: - Pacote changes: npm/pacote#259 - RFC: npm/rfcs#626 Signed-off-by: Philip Harrison <[email protected]>
1 parent d20ee2a commit 79bfd03

File tree

6 files changed

+610
-21
lines changed

6 files changed

+610
-21
lines changed

lib/commands/audit.js

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class VerifySignatures {
2424
this.missing = []
2525
this.checkedPackages = new Set()
2626
this.auditedWithKeysCount = 0
27-
this.verifiedCount = 0
27+
this.verifiedSignatureCount = 0
28+
this.verifiedAttestationCount = 0
2829
this.exitCode = 0
2930
}
3031

@@ -78,12 +79,25 @@ class VerifySignatures {
7879
this.npm.output(timing)
7980
this.npm.output('')
8081

81-
if (this.verifiedCount) {
82-
const verifiedBold = this.npm.chalk.bold('verified')
83-
if (this.verifiedCount === 1) {
84-
this.npm.output(`${this.verifiedCount} package has a ${verifiedBold} registry signature`)
82+
const verifiedBold = this.npm.chalk.bold('verified')
83+
if (this.verifiedSignatureCount) {
84+
if (this.verifiedSignatureCount === 1) {
85+
/* eslint-disable-next-line max-len */
86+
this.npm.output(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`)
87+
} else {
88+
/* eslint-disable-next-line max-len */
89+
this.npm.output(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`)
90+
}
91+
this.npm.output('')
92+
}
93+
94+
if (this.verifiedAttestationCount) {
95+
if (this.verifiedAttestationCount === 1) {
96+
/* eslint-disable-next-line max-len */
97+
this.npm.output(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`)
8598
} else {
86-
this.npm.output(`${this.verifiedCount} packages have ${verifiedBold} registry signatures`)
99+
/* eslint-disable-next-line max-len */
100+
this.npm.output(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`)
87101
}
88102
this.npm.output('')
89103
}
@@ -110,19 +124,35 @@ class VerifySignatures {
110124
const invalidClr = this.npm.chalk.bold(this.npm.chalk.red('invalid'))
111125
// We can have either invalid signatures or invalid provenance
112126
const invalidSignatures = this.invalid.filter(i => i.code === 'EINTEGRITYSIGNATURE')
113-
if (invalidSignatures.length === 1) {
114-
this.npm.output(`1 package has an ${invalidClr} registry signature:`)
115-
// } else if (invalidSignatures.length > 1) {
116-
} else {
117-
// TODO move this back to an else if once provenance attestation audit is added
118-
/* eslint-disable-next-line max-len */
119-
this.npm.output(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`)
127+
if (invalidSignatures.length) {
128+
if (invalidSignatures.length === 1) {
129+
this.npm.output(`1 package has an ${invalidClr} registry signature:`)
130+
} else {
131+
/* eslint-disable-next-line max-len */
132+
this.npm.output(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`)
133+
}
134+
this.npm.output('')
135+
invalidSignatures.map(i =>
136+
this.npm.output(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)
137+
)
138+
this.npm.output('')
120139
}
121-
this.npm.output('')
122-
invalidSignatures.map(i =>
123-
this.npm.output(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)
124-
)
125-
this.npm.output('')
140+
141+
const invalidAttestations = this.invalid.filter(i => i.code === 'EATTESTATIONVERIFY')
142+
if (invalidAttestations.length) {
143+
if (invalidAttestations.length === 1) {
144+
this.npm.output(`1 package has an ${invalidClr} attestation:`)
145+
} else {
146+
/* eslint-disable-next-line max-len */
147+
this.npm.output(`${invalidAttestations.length} packages have ${invalidClr} attestations:`)
148+
}
149+
this.npm.output('')
150+
invalidAttestations.map(i =>
151+
this.npm.output(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`)
152+
)
153+
this.npm.output('')
154+
}
155+
126156
if (invalid.length === 1) {
127157
/* eslint-disable-next-line max-len */
128158
this.npm.output(`Someone might have tampered with this package since it was published on the registry!`)
@@ -252,16 +282,19 @@ class VerifySignatures {
252282
const {
253283
_integrity: integrity,
254284
_signatures,
285+
_attestations,
255286
_resolved: resolved,
256287
} = await pacote.manifest(`${name}@${version}`, {
257288
verifySignatures: true,
289+
verifyAttestations: true,
258290
...this.buildRegistryConfig(registry),
259291
...this.npm.flatOptions,
260292
})
261293
const signatures = _signatures || []
262294
const result = {
263295
integrity,
264296
signatures,
297+
attestations: _attestations,
265298
resolved,
266299
}
267300
return result
@@ -287,14 +320,14 @@ class VerifySignatures {
287320
}
288321

289322
try {
290-
const { integrity, signatures, resolved } = await this.verifySignatures(
323+
const { integrity, signatures, attestations, resolved } = await this.verifySignatures(
291324
name, version, registry
292325
)
293326

294327
// Currently we only care about missing signatures on registries that provide a public key
295328
// We could make this configurable in the future with a strict/paranoid mode
296329
if (signatures.length) {
297-
this.verifiedCount += 1
330+
this.verifiedSignatureCount += 1
298331
} else if (keys.length) {
299332
this.missing.push({
300333
integrity,
@@ -305,17 +338,26 @@ class VerifySignatures {
305338
version,
306339
})
307340
}
341+
342+
// Track verified attestations separately to registry signatures, as all
343+
// packages on registries with signing keys are expected to have registry
344+
// signatures, but not all packages have provenance and publish attestations.
345+
if (attestations) {
346+
this.verifiedAttestationCount += 1
347+
}
308348
} catch (e) {
309-
if (e.code === 'EINTEGRITYSIGNATURE') {
349+
if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') {
310350
this.invalid.push({
311351
code: e.code,
352+
message: e.message,
312353
integrity: e.integrity,
313354
keyid: e.keyid,
314355
location,
315356
name,
316357
registry,
317358
resolved: e.resolved,
318359
signature: e.signature,
360+
predicateType: e.predicateType,
319361
type,
320362
version,
321363
})

package-lock.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11143,6 +11143,7 @@
1114311143
"version": "1.0.0",
1114411144
"resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.0.0.tgz",
1114511145
"integrity": "sha512-e+qfbn/zf1+rCza/BhIA//Awmf0v1pa5HQS8Xk8iXrn9bgytytVLqYD0P7NSqZ6IELTgq+tcDvLPkQjNHyWLNg==",
11146+
"inBundle": true,
1114611147
"dependencies": {
1114711148
"make-fetch-happen": "^11.0.1",
1114811149
"tuf-js": "^1.0.0"
@@ -14066,6 +14067,7 @@
1406614067
"version": "1.0.0",
1406714068
"resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.0.0.tgz",
1406814069
"integrity": "sha512-1dxsQwESDzACJjTdYHQ4wJ1f/of7jALWKfJEHSBWUQB/5UTJUx9SW6GHXp4mZ1KvdBRJCpGjssoPFGi4hvw8/A==",
14070+
"inBundle": true,
1406914071
"dependencies": {
1407014072
"make-fetch-happen": "^11.0.1",
1407114073
"minimatch": "^6.1.0"

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ exports[`test/lib/commands/audit.js TAP audit signatures json output with invali
5353
"invalid": [
5454
{
5555
"code": "EINTEGRITYSIGNATURE",
56+
"message": "[email protected] has an invalid registry signature with keyid: SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA and signature: bogus",
5657
"integrity": "sha512-QqZ7VJ/8xPkS9s2IWB7Shj3qTJdcRyeXKbPQnsZjsPEwvutGv0EGeVchPcauoiDFJlGbZMFq5GDCurAGNSghJQ==",
5758
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
5859
"location": "node_modules/kms-demo",
@@ -76,11 +77,34 @@ exports[`test/lib/commands/audit.js TAP audit signatures json output with invali
7677
}
7778
`
7879

80+
exports[`test/lib/commands/audit.js TAP audit signatures json output with invalid attestations > must match snapshot 1`] = `
81+
{
82+
"invalid": [
83+
{
84+
"code": "EATTESTATIONVERIFY",
85+
"message": "[email protected] failed to verify attestation: artifact signature verification failed",
86+
"integrity": "sha512-e+qfbn/zf1+rCza/BhIA//Awmf0v1pa5HQS8Xk8iXrn9bgytytVLqYD0P7NSqZ6IELTgq+tcDvLPkQjNHyWLNg==",
87+
"keyid": "",
88+
"location": "node_modules/sigstore",
89+
"name": "sigstore",
90+
"registry": "https://registry.npmjs.org/",
91+
"resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.0.0.tgz",
92+
"signature": "MEYCIQD10kAn3lC/1rJvXBtSDckbqkKEmz369gPDKb4lG4zMKQIhAP1+RhbMcASsfXhxpXKNCAjJb+3Av3Br95eKD7VL/BEB",
93+
"predicateType": "https://slsa.dev/provenance/v0.2",
94+
"type": "dependencies",
95+
"version": "1.0.0"
96+
}
97+
],
98+
"missing": []
99+
}
100+
`
101+
79102
exports[`test/lib/commands/audit.js TAP audit signatures json output with invalid signatures > must match snapshot 1`] = `
80103
{
81104
"invalid": [
82105
{
83106
"code": "EINTEGRITYSIGNATURE",
107+
"message": "[email protected] has an invalid registry signature with keyid: SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA and signature: bogus",
84108
"integrity": "sha512-QqZ7VJ/8xPkS9s2IWB7Shj3qTJdcRyeXKbPQnsZjsPEwvutGv0EGeVchPcauoiDFJlGbZMFq5GDCurAGNSghJQ==",
85109
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
86110
"location": "node_modules/kms-demo",
@@ -173,6 +197,17 @@ audited 1 package in xxx
173197
174198
`
175199

200+
exports[`test/lib/commands/audit.js TAP audit signatures with invalid attestations > must match snapshot 1`] = `
201+
audited 1 package in xxx
202+
203+
1 package has an invalid attestation:
204+
205+
[email protected] (https://registry.npmjs.org/)
206+
207+
Someone might have tampered with this package since it was published on the registry!
208+
209+
`
210+
176211
exports[`test/lib/commands/audit.js TAP audit signatures with invalid signatures > must match snapshot 1`] = `
177212
audited 1 package in xxx
178213
@@ -203,6 +238,18 @@ audited 1 package in xxx
203238
[email protected] (https://registry.npmjs.org/)
204239
`
205240

241+
exports[`test/lib/commands/audit.js TAP audit signatures with multiple invalid attestations > must match snapshot 1`] = `
242+
audited 2 packages in xxx
243+
244+
2 packages have invalid attestations:
245+
246+
[email protected] (https://registry.npmjs.org/)
247+
[email protected] (https://registry.npmjs.org/)
248+
249+
Someone might have tampered with these packages since they were published on the registry!
250+
251+
`
252+
206253
exports[`test/lib/commands/audit.js TAP audit signatures with multiple invalid signatures > must match snapshot 1`] = `
207254
audited 2 packages in xxx
208255
@@ -247,6 +294,15 @@ audited 2 packages in xxx
247294
[email protected] (https://registry.npmjs.org/)
248295
`
249296

297+
exports[`test/lib/commands/audit.js TAP audit signatures with valid attestations > must match snapshot 1`] = `
298+
audited 1 package in xxx
299+
300+
1 package has a verified registry signature
301+
302+
1 package has a verified attestation
303+
304+
`
305+
250306
exports[`test/lib/commands/audit.js TAP audit signatures with valid signatures > must match snapshot 1`] = `
251307
audited 1 package in xxx
252308

0 commit comments

Comments
 (0)