From 0501948ea02acfcc6a13f22b44f1a0aeeb3b7025 Mon Sep 17 00:00:00 2001 From: ajayk Date: Mon, 23 Feb 2026 16:40:38 -0800 Subject: [PATCH] fix: skip registry key check for keyless (Sigstore/Fulcio) attestations Attestations signed with keyless Sigstore/Fulcio have no keyid and embed the signing certificate directly in the bundle. The existing guard unconditionally required matching registry keys, causing EMISSINGSIGNATUREKEY for registries that only use keyless signing. Only throw when there are keyed attestations that can't be matched. --- lib/registry.js | 5 ++++- test/registry.js | 54 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/lib/registry.js b/lib/registry.js index 1ecf4ee1..954a1640 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -256,7 +256,10 @@ class RegistryFetcher extends Fetcher { const attestationKeyIds = bundles.map((b) => b.keyid).filter((k) => !!k) const attestationRegistryKeys = (this.registryKeys || []) .filter(key => attestationKeyIds.includes(key.keyid)) - if (!attestationRegistryKeys.length) { + // Only require registry keys when there are keyed attestations. + // Keyless (Sigstore/Fulcio) attestations embed their signing + // certificate in the bundle and don't need registry keys. + if (attestationKeyIds.length > 0 && !attestationRegistryKeys.length) { throw Object.assign(new Error( `${mani._id} has attestations but no corresponding public key(s) can be found` ), { code: 'EMISSINGSIGNATUREKEY' }) diff --git a/test/registry.js b/test/registry.js index 1a1bc652..ac0ba646 100644 --- a/test/registry.js +++ b/test/registry.js @@ -661,14 +661,54 @@ t.test('verifyAttestations no attestation with keyid', async t => { }], }) - return t.rejects( - f.manifest(), - // eslint-disable-next-line max-len - /sigstore@0\.4\.0 has attestations but no corresponding public key\(s\) can be found/, - { - code: 'EMISSINGSIGNATUREKEY', - } + // Keyless attestations (no keyid) should not require registry keys + const mani = await f.manifest() + t.ok(mani._attestations) + t.ok(mani._integrity) +}) + +t.test('verifyAttestations keyless without registry keys', async t => { + tnock(t, 'https://registry.npmjs.org') + .get('/sigstore') + .reply(200, { + _id: 'sigstore', + _rev: 'deadbeef', + name: 'sigstore', + 'dist-tags': { latest: '0.4.0' }, + versions: { + '0.4.0': { + name: 'sigstore', + version: '0.4.0', + dist: { + // eslint-disable-next-line max-len + integrity: 'sha512-KCwMX6k20mQyFkNYG2XT3lwK9u1P36wS9YURFd85zCXPrwrSLZCEh7/vMBFNYcJXRiBtGDS+T4/RZZF493zABA==', + // eslint-disable-next-line max-len + attestations: { url: 'https://registry.npmjs.org/-/npm/v1/attestations/sigstore@0.4.0', provenance: { predicateType: 'https://slsa.dev/provenance/v0.2' } }, + }, + }, + }, + }) + + const fixture = fs.readFileSync( + path.join(__dirname, 'fixtures', 'sigstore/no-keyid-attestations.json'), + 'utf8' ) + + tnock(t, 'https://registry.npmjs.org') + .get('/-/npm/v1/attestations/sigstore@0.4.0') + .reply(200, JSON.parse(fixture)) + + // Keyless (Sigstore/Fulcio) attestations embed the signing certificate + // in the bundle and should verify without any registry keys at all + const f = new MockedRegistryFetcher('sigstore@0.4.0', { + registry: 'https://registry.npmjs.org', + cache, + verifyAttestations: true, + }) + + const mani = await f.manifest() + t.ok(mani._attestations) + t.ok(mani._integrity) }) t.test('verifyAttestations valid attestations', async t => {