/** * StoreKit 2 signed-transaction verification. * * EXAMPLE CODE. Placeholders only. * * Verify that: * - the x5c chain is internally consistent (leaf <- intermediate <- root), * - the root's SHA-256 matches the pinned Apple Root CA - G3, and * - the JWS is signed by the leaf certificate. * * The crypto dependencies are imported lazily so the pure logic in * entitlement.ts stays unit-testable with zero installs. Production * dependencies: `jose`, `@peculiar/x509`. */ import type { JWSTransaction } from "./transaction.ts"; /** Verifier signature, so tests can swap in a fake and skip real certificates. */ export type TransactionVerifier = ( jws: string, appleRootSha256: string, ) => Promise; export const verifyTransactionJWS: TransactionVerifier = async ( jws, appleRootSha256, ) => { const { decodeProtectedHeader, importX509, jwtVerify } = await import("jose"); const x509 = await import("@peculiar/x509"); const header = decodeProtectedHeader(jws); const x5c = header.x5c; if (!Array.isArray(x5c) || x5c.length < 2) { throw new Error("JWS is missing an x5c certificate chain"); } // Verify each link in the chain (leaf first, root last). const certs = x5c.map((der) => new x509.X509Certificate(der)); for (let i = 0; i < certs.length - 1; i++) { const issuerKey = await certs[i + 1].publicKey.export(); if (!(await certs[i].verify({ publicKey: issuerKey }))) { throw new Error("broken certificate chain"); } } // Pin the trust anchor: the root must be Apple Root CA - G3. const root = certs[certs.length - 1]; const rootSha256 = toHex(await crypto.subtle.digest("SHA-256", root.rawData)); if (rootSha256 !== normalizeHex(appleRootSha256)) { throw new Error("untrusted root certificate"); } // Verify the JWS signature with the leaf certificate's public key. const leafKey = await importX509(certs[0].toString("pem"), "ES256"); const { payload } = await jwtVerify(jws, leafKey); return payload as JWSTransaction; }; function toHex(buf: ArrayBuffer): string { return [...new Uint8Array(buf)] .map((b) => b.toString(16).padStart(2, "0")) .join(""); } function normalizeHex(s: string): string { return s.toLowerCase().replace(/[^0-9a-f]/g, ""); }