Skip to content

Commit 4426411

Browse files
authored
fix: npm audit signatures for keyless attestation registries (#9026)
npm audit signatures fails when a registry only uses keyless (Sigstore/Fulcio) attestations and doesn't provide registry signing keys. The[ auditedWithKeysCount guard in verify-signatures.js ](https://github.com/npm/cli/blob/latest/lib/utils/verify-signatures.js#L48) treats any registry without keys as unsupported, even though keyless attestations don't need registry keys at all -- the signing certificate is embedded directly in the bundle and verified through Sigstore's TUF root of trust. This updates the check to also accept verified keyless attestations as a valid audit result, so registries that exclusively use Fulcio-based signing (like Chainguard) work correctly with npm audit signatures. Before this change: npm error found no dependencies to audit that were installed from a supported registry After: audited 1 package in 1s 1 package has a verified attestation This change works together with the corresponding pacote fixes ([pacote/pull/454](npm/pacote#454)) ([pacote/pull/452](npm/pacote#452)) which allows keyless attestation bundles to pass the registry key matching check.
1 parent 7798b6e commit 4426411

File tree

3 files changed

+96
-1
lines changed

3 files changed

+96
-1
lines changed

lib/utils/verify-signatures.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class VerifySignatures {
4545

4646
// Didn't find any dependencies that could be verified, e.g. only local
4747
// deps, missing version, not on a registry etc.
48-
if (!this.auditedWithKeysCount) {
48+
if (!this.auditedWithKeysCount && !this.verifiedAttestationCount) {
4949
throw new Error('found no dependencies to audit that were installed from ' +
5050
'a supported registry')
5151
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"attestations": [
3+
{
4+
"predicateType": "https://slsa.dev/provenance/v0.2",
5+
"bundle": {
6+
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1",
7+
"verificationMaterial": {
8+
"x509CertificateChain": {
9+
"certificates": [
10+
{
11+
"rawBytes": "MIIDmzCCAyGgAwIBAgIUce0wM1Ev1pqBXH9W1BbvEg9RopYwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwMjA5MTc1NjAwWhcNMjMwMjA5MTgwNjAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0SgHPgKy+HgMtXdqkkgY6Ji1v7wc+lxnnatY73cbKFwQzw+/x8288IwIz6y54dtznSXnjbzNMdNS0Q2rfMsMcaOCAkAwggI8MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUVVMaFO5X4Xzc/tiJCfm0WXa75QIwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wZAYDVR0RAQH/BFowWIZWaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3B1Ymxpc2gueW1sQHJlZnMvdGFncy92MS4wLjAwOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAVBgorBgEEAYO/MAECBAdyZWxlYXNlMDYGCisGAQQBg78wAQMEKDA2NTI4OTk3YzNjOGFiOWY4NjRmYWQ5YTM2NTg0NDZhY2E3ZmQ4NmQwFQYKKwYBBAGDvzABBAQHcHVibGlzaDAiBgorBgEEAYO/MAEFBBRzaWdzdG9yZS9zaWdzdG9yZS1qczAeBgorBgEEAYO/MAEGBBByZWZzL3RhZ3MvdjEuMC4wMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGGN1HpGQAABAMARjBEAiB/E0+AFpKimPxI/TQDXeCa06+wtpwvLhyPrbHOQYu74gIgB/9fdZD+uHvUBHyptxaGoBxdJfUKEYx9nhaZw2LeZuwwCgYIKoZIzj0EAwMDaAAwZQIxAPW070C7IM0RrAU5rMpP25TFH/rfKvbvqRNnUoPfvIlA9q7Abe8BHIl97pTmf/5vJgIwbZ4myRXGWjB0LUyzplC2GX0kklVGYeqRM5xxsxAK0zwd/U7KjoFIlp/gkyLyiMHH"
12+
},
13+
{
14+
"rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
15+
},
16+
{
17+
"rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
18+
}
19+
]
20+
},
21+
"tlogEntries": [
22+
{
23+
"logIndex": "12988397",
24+
"logId": {
25+
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
26+
},
27+
"kindVersion": {
28+
"kind": "intoto",
29+
"version": "0.0.2"
30+
},
31+
"integratedTime": "1675965360",
32+
"inclusionPromise": {
33+
"signedEntryTimestamp": "MEUCID8bfktgHysxMJAIXz6CqqKHGAYPp/X6FZrS9SDtKdbcAiEAg+0zUFNPJKEVX6m33aCU+CRBgWkDNOC8oE4jHoco4kw="
34+
},
35+
"inclusionProof": null,
36+
"canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVVJ0ZWtORFFYbEhaMEYzU1VKQlowbFZZMlV3ZDAweFJYWXhjSEZDV0VnNVZ6RkNZblpGWnpsU2IzQlpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDAxcVFUVk5WR014VG1wQmQxZG9ZMDVOYWsxM1RXcEJOVTFVWjNkT2FrRjNWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVVd1UyZElVR2RMZVN0SVowMTBXR1J4YTJ0bldUWkthVEYyTjNkaksyeDRibTVoZEZrS056TmpZa3RHZDFGNmR5c3ZlRGd5T0RoSmQwbDZObmsxTkdSMGVtNVRXRzVxWW5wT1RXUk9VekJSTW5KbVRYTk5ZMkZQUTBGclFYZG5aMGs0VFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWV1ZrMWhDa1pQTlZnMFdIcGpMM1JwU2tObWJUQlhXR0UzTlZGSmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxcEJXVVJXVWpCU1FWRklMMEpHYjNkWFNWcFhZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wSXhXVzE0Y0dNeVozVmxWekZ6VVVoS2JGcHVUWFprUjBadUNtTjVPVEpOVXpSM1RHcEJkMDlSV1V0TGQxbENRa0ZIUkhaNlFVSkJVVkZ5WVVoU01HTklUVFpNZVRrd1lqSjBiR0pwTldoWk0xSndZakkxZWt4dFpIQUtaRWRvTVZsdVZucGFXRXBxWWpJMU1GcFhOVEJNYlU1MllsUkJWa0puYjNKQ1owVkZRVmxQTDAxQlJVTkNRV1I1V2xkNGJGbFlUbXhOUkZsSFEybHpSd3BCVVZGQ1p6YzRkMEZSVFVWTFJFRXlUbFJKTkU5VWF6TlplazVxVDBkR2FVOVhXVFJPYWxKdFdWZFJOVmxVVFRKT1ZHY3dUa1JhYUZreVJUTmFiVkUwQ2s1dFVYZEdVVmxMUzNkWlFrSkJSMFIyZWtGQ1FrRlJTR05JVm1saVIyeDZZVVJCYVVKbmIzSkNaMFZGUVZsUEwwMUJSVVpDUWxKNllWZGtlbVJIT1hrS1dsTTVlbUZYWkhwa1J6bDVXbE14Y1dONlFXVkNaMjl5UW1kRlJVRlpUeTlOUVVWSFFrSkNlVnBYV25wTU0xSm9Xak5OZG1ScVJYVk5RelIzVFVsSFNncENaMjl5UW1kRlJVRmtXalZCWjFGRFFraHpSV1ZSUWpOQlNGVkJNMVF3ZDJGellraEZWRXBxUjFJMFkyMVhZek5CY1VwTFdISnFaVkJMTXk5b05IQjVDbWRET0hBM2J6UkJRVUZIUjA0eFNIQkhVVUZCUWtGTlFWSnFRa1ZCYVVJdlJUQXJRVVp3UzJsdFVIaEpMMVJSUkZobFEyRXdOaXQzZEhCM2RreG9lVkFLY21KSVQxRlpkVGMwWjBsblFpODVabVJhUkN0MVNIWlZRa2g1Y0hSNFlVZHZRbmhrU21aVlMwVlplRGx1YUdGYWR6Sk1aVnAxZDNkRFoxbEpTMjlhU1FwNmFqQkZRWGROUkdGQlFYZGFVVWw0UVZCWE1EY3dRemRKVFRCU2NrRlZOWEpOY0ZBeU5WUkdTQzl5Wmt0MlluWnhVazV1Vlc5UVpuWkpiRUU1Y1RkQkNtSmxPRUpJU1d3NU4zQlViV1l2TlhaS1owbDNZbG8wYlhsU1dFZFhha0l3VEZWNWVuQnNRekpIV0RCcmEyeFdSMWxsY1ZKTk5YaDRjM2hCU3pCNmQyUUtMMVUzUzJwdlJrbHNjQzluYTNsTWVXbE5TRWdLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89Iiwic2lnIjoiVFVWWlEwbFJSREV3YTBGdU0yeERMekZ5U25aWVFuUlRSR05yWW5GclMwVnRlak0yT1dkUVJFdGlOR3hITkhwTlMxRkphRUZRTVN0U2FHSk5ZMEZUYzJaWWFIaHdXRXRPUTBGcVNtSXJNMEYyTTBKeU9UVmxTMFEzVmt3dlFrVkMifV19LCJoYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNjI2OWQzNzQ2MzI0MjM5YzEzOGJkZTMzMDEzMTVlY2FkNmI4ZGM5YzcwY2RlYTBhODEyYjYzYTUzOGJmYzdlYyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6Ijg0NWU5MTRlYmJhZTBkNmZmY2FlMmFmYjc3YzdkZWY0NzkzMDVjOWVlMzExMDE0MDJmZTQ3NWU2ZDIzZjExYzkifX19fQ=="
37+
}
38+
],
39+
"timestampVerificationData": null
40+
},
41+
"dsseEnvelope": {
42+
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInN1YmplY3QiOlt7Im5hbWUiOiJwa2c6bnBtL3NpZ3N0b3JlQDEuMC4wIiwiZGlnZXN0Ijp7InNoYTUxMiI6IjdiZWE5ZjZlN2ZmMzdmNWZhYjBiMzZiZjA2MTIwMGZmZjAzMDk5ZmQyZmQ2OTZiOTFkMDRiYzVlNGYyMjVlYjlmZDZlMGNhZGNhZDU0YmE5ODBmNDNmYjM1MmE5OWU4ODEwYjRlMGFiZWI1YzBlZjJjZjkxMDhjZDFmMjU4YjM2In19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MC4yIiwicHJlZGljYXRlIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9ucG0vY2xpL2doYUB2MCIsImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbnBtL2NsaUA5LjQuMiJ9LCJpbnZvY2F0aW9uIjp7ImNvbmZpZ1NvdXJjZSI6eyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvdGFncy92MS4wLjAiLCJkaWdlc3QiOnsic2hhMSI6IjA2NTI4OTk3YzNjOGFiOWY4NjRmYWQ5YTM2NTg0NDZhY2E3ZmQ4NmQifSwiZW50cnlQb2ludCI6InNpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3B1Ymxpc2gueW1sQHJlZnMvdGFncy92MS4wLjAifSwicGFyYW1ldGVycyI6e30sImVudmlyb25tZW50Ijp7IkdJVEhVQl9BQ1RPUl9JRCI6IjM5ODAyNyIsIkdJVEhVQl9FVkVOVF9OQU1FIjoicmVsZWFzZSIsIkdJVEhVQl9SRUYiOiJyZWZzL3RhZ3MvdjEuMC4wIiwiR0lUSFVCX1JFRl9UWVBFIjoidGFnIiwiR0lUSFVCX1JFUE9TSVRPUlkiOiJzaWdzdG9yZS9zaWdzdG9yZS1qcyIsIkdJVEhVQl9SRVBPU0lUT1JZX0lEIjoiNDk1NTc0NTU1IiwiR0lUSFVCX1JFUE9TSVRPUllfT1dORVJfSUQiOiI3MTA5NjM1MyIsIkdJVEhVQl9SVU5fQVRURU1QVCI6IjEiLCJHSVRIVUJfUlVOX0lEIjoiNDEzNzAyODgxNiIsIkdJVEhVQl9SVU5fTlVNQkVSIjoiOSIsIkdJVEhVQl9TSEEiOiIwNjUyODk5N2MzYzhhYjlmODY0ZmFkOWEzNjU4NDQ2YWNhN2ZkODZkIiwiR0lUSFVCX1dPUktGTE9XX1JFRiI6InNpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3B1Ymxpc2gueW1sQHJlZnMvdGFncy92MS4wLjAiLCJHSVRIVUJfV09SS0ZMT1dfU0hBIjoiMDY1Mjg5OTdjM2M4YWI5Zjg2NGZhZDlhMzY1ODQ0NmFjYTdmZDg2ZCJ9fSwibWV0YWRhdGEiOnsiYnVpbGRJbnZvY2F0aW9uSWQiOiI0MTM3MDI4ODE2LTEiLCJjb21wbGV0ZW5lc3MiOnsicGFyYW1ldGVycyI6ZmFsc2UsImVudmlyb25tZW50IjpmYWxzZSwibWF0ZXJpYWxzIjpmYWxzZX0sInJlcHJvZHVjaWJsZSI6ZmFsc2V9LCJtYXRlcmlhbHMiOlt7InVyaSI6ImdpdCtodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMiLCJkaWdlc3QiOnsic2hhMSI6IjA2NTI4OTk3YzNjOGFiOWY4NjRmYWQ5YTM2NTg0NDZhY2E3ZmQ4NmQifX1dfX0=",
43+
"payloadType": "application/vnd.in-toto+json",
44+
"signatures": [
45+
{
46+
"sig": "MEYCIQD10kAn3lC/1rJvXBtSDckbqkKEmz369gPDKb4lG4zMKQIhAP1+RhbMcASsfXhxpXKNCAjJb+3Av3Br95eKD7VL/BEB",
47+
"keyid": ""
48+
}
49+
]
50+
}
51+
}
52+
}
53+
]
54+
}

test/lib/commands/audit.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,47 @@ t.test('audit signatures', async t => {
18531853
t.matchSnapshot(joinedOutput())
18541854
})
18551855

1856+
t.test('with keyless attestations and no registry keys', async t => {
1857+
const { npm, joinedOutput } = await loadMockNpm(t, {
1858+
prefixDir: installWithValidAttestations,
1859+
mocks: {
1860+
pacote: t.mock('pacote', {
1861+
sigstore: { verify: async () => true },
1862+
}),
1863+
},
1864+
})
1865+
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
1866+
const manifest = registry.manifest({
1867+
name: 'sigstore',
1868+
packuments: [{
1869+
version: '1.0.0',
1870+
dist: {
1871+
integrity: 'sha512-e+qfbn/zf1+rCza/BhIA//Awmf0v1pa5HQS8Xk8iXrn9bgytytVLqYD' +
1872+
'0P7NSqZ6IELTgq+tcDvLPkQjNHyWLNg==',
1873+
tarball: 'https://registry.npmjs.org/sigstore/-/sigstore-1.0.0.tgz',
1874+
attestations: {
1875+
url: 'https://registry.npmjs.org/-/npm/v1/attestations/sigstore@1.0.0',
1876+
provenance: { predicateType: 'https://slsa.dev/provenance/v0.2' },
1877+
},
1878+
},
1879+
}],
1880+
})
1881+
await registry.package({ manifest })
1882+
const fixture = fs.readFileSync(
1883+
path.resolve(__dirname, '../../fixtures/sigstore/keyless-sigstore-attestations.json'),
1884+
'utf8'
1885+
)
1886+
registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, JSON.parse(fixture))
1887+
// TUF returns no keys and the keys endpoint returns 404
1888+
mockTUF({ npm, target: TUF_TARGET_NOT_FOUND })
1889+
registry.nock.get('/-/npm/v1/keys').reply(404)
1890+
1891+
await npm.exec('audit', ['signatures'])
1892+
1893+
t.notOk(process.exitCode, 'should exit successfully')
1894+
t.match(joinedOutput(), /1 package has a verified attestation/)
1895+
})
1896+
18561897
t.test('with multiple valid attestations', async t => {
18571898
const { npm, joinedOutput } = await loadMockNpm(t, {
18581899
prefixDir: installWithMultipleValidAttestations,

0 commit comments

Comments
 (0)