Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions src/keyResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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 };
}
}

Expand Down Expand Up @@ -115,16 +137,24 @@ export async function resolvePublicKey(options: ResolveKeyOptions): Promise<Reso
}

const payload = (await response.json()) as unknown;
const keyText = pickKeyFromPayload(payload, signatureKeyId);
if (!keyText) {
const keyResult = pickKeyFromPayload(payload, signatureKeyId);
if (!keyResult) {
return {
ok: false,
errorCode: 'KEY_RESOLUTION_FAILED',
errorMessage: `no key found for signature key id "${signatureKeyId}"`,
};
}

return { ok: true, key: asPublicKey(keyText), keySource: finalKeyUrl };
if (!keyResult.ok) {
return {
ok: false,
errorCode: 'KEY_RESOLUTION_FAILED',
errorMessage: keyResult.errorMessage,
};
}

return { ok: true, key: asPublicKey(keyResult.keyText), keySource: finalKeyUrl };
} catch (error) {
return {
ok: false,
Expand Down
40 changes: 40 additions & 0 deletions tests/verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ async function loadJson(name: string): Promise<unknown> {
}

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<string> {
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 () => {
Expand Down Expand Up @@ -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 });
Expand Down