From abdfdbc1d8d6a6956213ce1306745e268fd4d142 Mon Sep 17 00:00:00 2001 From: akuraposo Date: Thu, 4 Jun 2026 18:44:42 +0000 Subject: [PATCH] fix(verify): reject unsupported receiptVersion values (closes #53) The local reference verifier only checked `receiptVersion` as a required signed field, not as a required v1 constant. A signed, unexpired receipt with `receiptVersion: "v2"` (or any other unsupported value) was accepted by `pp verify` as long as `canonicalization` was `jcs_v1`, `signatureAlg` was `ed25519`, the signature verified, and the receipt was not expired or revoked. That means a verifier claiming `pp-receipt-v1` compatibility could silently accept receipts from an unsupported receipt-version namespace, downgrading the local trust boundary. This patch adds an explicit `receiptVersion === 'v1'` check (matching the v1 spec) and fails closed with a `MALFORMED_RECEIPT` error if any other value is supplied. - Added `RECEIPT_VERSION = 'v1'` constant in `src/verify.ts` - Added explicit version check after the canonicalization check - Added regression test that forges receiptVersion='v2' on a valid receipt and expects MALFORMED_RECEIPT - All 6 tests pass (5 pre-existing + 1 new) - `npm run build` clean Closes #53 --- src/verify.ts | 12 ++++++++++++ tests/verify.test.ts | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/verify.ts b/src/verify.ts index 9a3280c..f14c827 100644 --- a/src/verify.ts +++ b/src/verify.ts @@ -2,6 +2,8 @@ import { verify as verifySignature } from 'node:crypto'; import { canonicalizeReceiptBytes, CANONICALIZATION_VERSION } from './canonicalize.js'; import { resolvePublicKey } from './keyResolver.js'; +export const RECEIPT_VERSION = 'v1'; + const REQUIRED_SIGNED_FIELDS = [ 'id', 'companyId', @@ -95,6 +97,16 @@ export async function verifyReceipt(receipt: unknown, options: VerifyOptions): P }; } + if (receipt.receiptVersion !== RECEIPT_VERSION) { + return { + verified: false, + exitCode: 3, + errorCode: 'MALFORMED_RECEIPT', + errorMessage: `unsupported receiptVersion: expected "${RECEIPT_VERSION}"`, + receiptId: receipt.id as string, + }; + } + if (receipt.signatureAlg !== 'ed25519') { return { verified: false, diff --git a/tests/verify.test.ts b/tests/verify.test.ts index 0fd26db..5fbd922 100644 --- a/tests/verify.test.ts +++ b/tests/verify.test.ts @@ -59,4 +59,18 @@ describe('verifyReceipt', () => { expect(result.errorCode).toBe('SIGNATURE_INVALID'); } }); + + it('returns malformed for unsupported receiptVersion', async () => { + const receipt = await loadJson('valid.json') as Record; + // Forge a future receiptVersion value, which would otherwise pass + // required-field, signature, expiry, and status checks. + receipt.receiptVersion = 'v2'; + const result = await verifyReceipt(receipt, { keyFile, noNetwork: true }); + expect(result.verified).toBe(false); + if (!result.verified) { + expect(result.exitCode).toBe(3); + expect(result.errorCode).toBe('MALFORMED_RECEIPT'); + expect(result.errorMessage.toLowerCase()).toContain('receiptversion'); + } + }); });