From 7469f2faf6beabcb75d8c43ae07a9763327cad08 Mon Sep 17 00:00:00 2001 From: Mirochill <200482516+Mirochill@users.noreply.github.com> Date: Sat, 30 May 2026 22:54:46 +0200 Subject: [PATCH] fix: reject revoked remotely resolved keys --- src/keyResolver.ts | 42 ++++++++++++++++++++++++++++++++++++------ tests/verify.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/keyResolver.ts b/src/keyResolver.ts index 8a8b65a..cc7db5e 100644 --- a/src/keyResolver.ts +++ b/src/keyResolver.ts @@ -39,9 +39,14 @@ type RemoteKeyRecord = { id?: string; publicKey?: string; key?: string; + status?: unknown; }; -function pickKeyFromPayload(payload: unknown, signatureKeyId: string): string | undefined { +type PickKeyResult = + | { ok: true; keyText: string } + | { ok: false; errorMessage: string }; + +function pickKeyFromPayload(payload: unknown, signatureKeyId: string): PickKeyResult | undefined { if (!payload || typeof payload !== 'object') { return undefined; } @@ -59,11 +64,28 @@ function pickKeyFromPayload(payload: unknown, signatureKeyId: string): string | if (!id || id !== signatureKeyId) { continue; } + + if (candidate.status !== undefined) { + if (candidate.status !== 'active' && candidate.status !== 'rotated' && candidate.status !== 'revoked') { + return { + ok: false, + errorMessage: `key "${signatureKeyId}" has unsupported status "${String(candidate.status)}"`, + }; + } + + if (candidate.status === 'revoked') { + return { + ok: false, + errorMessage: `key "${signatureKeyId}" is revoked`, + }; + } + } + if (typeof candidate.publicKey === 'string') { - return candidate.publicKey; + return { ok: true, keyText: candidate.publicKey }; } if (typeof candidate.key === 'string') { - return candidate.key; + return { ok: true, keyText: candidate.key }; } } @@ -115,8 +137,8 @@ export async function resolvePublicKey(options: ResolveKeyOptions): Promise { } const keyFile = resolve(process.cwd(), 'tests/fixtures/public-key.pem'); +const remoteKeyFile = resolve(process.cwd(), 'tests/fixtures/public-key.base64'); + +async function remoteKeyUrl(status?: unknown): Promise { + const publicKey = (await readFile(remoteKeyFile, 'utf8')).trim(); + const payload = { + keyId: 'pp-test-2026-q2', + algorithm: 'ed25519', + publicKey, + ...(status === undefined ? {} : { status }), + }; + return `data:application/json,${encodeURIComponent(JSON.stringify(payload))}`; +} describe('verifyReceipt', () => { it('verifies a valid receipt', async () => { @@ -50,6 +62,34 @@ describe('verifyReceipt', () => { } }); + it('verifies a receipt with a remotely resolved rotated key', async () => { + const receipt = await loadJson('valid.json'); + const result = await verifyReceipt(receipt, { keyUrl: await remoteKeyUrl('rotated') }); + expect(result.verified).toBe(true); + }); + + it('rejects a remotely resolved revoked key', async () => { + const receipt = await loadJson('valid.json'); + const result = await verifyReceipt(receipt, { keyUrl: await remoteKeyUrl('revoked') }); + expect(result.verified).toBe(false); + if (!result.verified) { + expect(result.exitCode).toBe(4); + expect(result.errorCode).toBe('KEY_RESOLUTION_FAILED'); + expect(result.errorMessage).toBe('key "pp-test-2026-q2" is revoked'); + } + }); + + it('rejects an explicit unknown remote key status', async () => { + const receipt = await loadJson('valid.json'); + const result = await verifyReceipt(receipt, { keyUrl: await remoteKeyUrl('disabled') }); + expect(result.verified).toBe(false); + if (!result.verified) { + expect(result.exitCode).toBe(4); + expect(result.errorCode).toBe('KEY_RESOLUTION_FAILED'); + expect(result.errorMessage).toBe('key "pp-test-2026-q2" has unsupported status "disabled"'); + } + }); + it('returns signature invalid for tampered receipt', async () => { const receipt = await loadJson('tampered.json'); const result = await verifyReceipt(receipt, { keyFile, noNetwork: true });