diff --git a/src/keyResolver.ts b/src/keyResolver.ts index 8a8b65a..716ff6b 100644 --- a/src/keyResolver.ts +++ b/src/keyResolver.ts @@ -27,11 +27,19 @@ export type ResolveKeyResult = ResolveKeySuccess | ResolveKeyFailure; function asPublicKey(raw: string): ReturnType { const trimmed = raw.trim(); + let key: ReturnType; if (trimmed.includes('BEGIN PUBLIC KEY')) { - return createPublicKey(trimmed); + key = createPublicKey(trimmed); + } else { + const der = Buffer.from(trimmed, 'base64'); + key = createPublicKey({ key: der, format: 'der', type: 'spki' }); } - const der = Buffer.from(trimmed, 'base64'); - return createPublicKey({ key: der, format: 'der', type: 'spki' }); + if (key.asymmetricKeyType !== 'ed25519') { + throw new Error( + `public key is ${key.asymmetricKeyType}; only ed25519 signing keys are accepted` + ); + } + return key; } type RemoteKeyRecord = { diff --git a/tests/verify.test.ts b/tests/verify.test.ts index 0fd26db..e24347d 100644 --- a/tests/verify.test.ts +++ b/tests/verify.test.ts @@ -1,7 +1,12 @@ -import { readFile } from 'node:fs/promises'; +import { readFile, writeFile, unlink } from 'node:fs/promises'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { resolve } from 'node:path'; +import { generateKeyPairSync } from 'node:crypto'; import { describe, expect, it } from 'vitest'; import { verifyReceipt } from '../src/verify.js'; +import { resolvePublicKey } from '../src/keyResolver.js'; async function loadJson(name: string): Promise { const path = resolve(process.cwd(), 'tests/fixtures', name); @@ -60,3 +65,53 @@ describe('verifyReceipt', () => { } }); }); + +describe('resolvePublicKey key-type enforcement', () => { + async function makeTmpKeyFile(keyPem: string): Promise { + const dir = await mkdtemp(join(tmpdir(), 'pp-test-')); + const keyPath = join(dir, 'key.pem'); + await writeFile(keyPath, keyPem); + return keyPath; + } + + async function cleanup(keyPath: string): Promise { + const dir = keyPath.replace(/\/[^/]+$/, ''); + try { + await rm(dir, { recursive: true, force: true }); + } catch { + // best-effort cleanup + } + } + + it('rejects RSA public keys as non-ed25519', async () => { + const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); + const pem = publicKey.export({ type: 'spki', format: 'pem' }); + const keyPath = await makeTmpKeyFile(pem); + try { + const result = await resolvePublicKey({ keyFile: keyPath, signatureKeyId: 'any' }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errorCode).toBe('KEY_RESOLUTION_FAILED'); + expect(result.errorMessage.toLowerCase()).toContain('rsa'); + } + } finally { + await cleanup(keyPath); + } + }); + + it('rejects ECDSA P-256 public keys as non-ed25519', async () => { + const { publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const pem = publicKey.export({ type: 'spki', format: 'pem' }); + const keyPath = await makeTmpKeyFile(pem); + try { + const result = await resolvePublicKey({ keyFile: keyPath, signatureKeyId: 'any' }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.errorCode).toBe('KEY_RESOLUTION_FAILED'); + expect(result.errorMessage.toLowerCase()).toContain('ec'); + } + } finally { + await cleanup(keyPath); + } + }); +});