import { describe, expect, it } from 'bun:test'; import { readdir, readFile } from 'node:fs/promises'; import { URL } from 'node:url'; import type { ParsedCertificate, ParsedCertificateRevocationList } from '#micro509'; import { checkChainRevocation, parseCertificateDer, parseCertificateRevocationListDerOrThrow, unwrap, verifyCertificateChain, } from '#micro509'; import { PKITS_CASES, type PkitsCase } from '#test/pkits/manifest'; const PKITS_VALIDATION_TIME = new Date('2011-04-15T00:00:00Z'); const REVOCATION_SECTIONS = new Set(['4.4', '4.5', '4.14', '4.15']); const REVOCATION_TEST_NUMBERS = new Set(['4.7.4', '4.7.5']); // PKITS cases that exercise DSA. micro509 is WebCrypto-native and does not // support DSA by design, so these "valid DSA" chains fail with // `unsupported_signature_algorithm_parameters`. Tracked with `it.failing` so the // suite flags us if DSA support ever lands (the test would start passing). const UNSUPPORTED_ALGORITHM_TESTS = new Set(['4.1.4', '4.1.5']); const certificateDerCache = new Map>(); const parsedCertificateCache = new Map>(); const parsedCrlCache = new Map>(); function shouldEvaluateRevocation(pkitsCase: PkitsCase): boolean { return ( REVOCATION_SECTIONS.has(pkitsCase.section) || REVOCATION_TEST_NUMBERS.has(pkitsCase.testNumber) ); } async function readPkitsFixture(path: string): Promise { return new Uint8Array(await readFile(new URL(path, import.meta.url))); } async function readPkitsCertificateDer(name: string): Promise { const cached = certificateDerCache.get(name); if (cached !== undefined) { return cached; } const pending = readPkitsFixture(`./fixtures/pkits/certs/${name}.crt`); certificateDerCache.set(name, pending); return await pending; } async function readPkitsParsedCertificate(name: string): Promise { const cached = parsedCertificateCache.get(name); if (cached !== undefined) { return cached; } const pending = readPkitsCertificateDer(name).then((der) => unwrap(parseCertificateDer(der))); parsedCertificateCache.set(name, pending); return await pending; } async function readPkitsParsedCrl(name: string): Promise { const cached = parsedCrlCache.get(name); if (cached !== undefined) { return cached; } const pending = readPkitsFixture(`./fixtures/pkits/crls/${name}.crl`).then((der) => parseCertificateRevocationListDerOrThrow(der), ); parsedCrlCache.set(name, pending); return await pending; } let allPkitsCertificatesCache: ParsedCertificate[] | null = null; async function loadAllPkitsCertificates(): Promise { if (allPkitsCertificatesCache !== null) { return allPkitsCertificatesCache; } const certDir = new URL('./fixtures/pkits/certs/', import.meta.url); const files = await readdir(certDir); const results = await Promise.allSettled( files .filter((f) => f.endsWith('.crt')) .map((f) => { const name = f.replace('.crt', ''); return readPkitsParsedCertificate(name); }), ); const certs = results .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') .map((r) => r.value); allPkitsCertificatesCache = certs; return certs; } async function evaluatePkitsRevocation( chain: readonly ParsedCertificate[], crlNames: readonly string[], ): Promise { const crlResults = await Promise.allSettled(crlNames.map(readPkitsParsedCrl)); const crls = crlResults .filter( (r): r is PromiseFulfilledResult => r.status === 'fulfilled', ) .map((r) => r.value); const result = await checkChainRevocation({ chain, crls, extraCertificates: await loadAllPkitsCertificates(), policy: { mode: 'hard-fail' }, at: PKITS_VALIDATION_TIME, }); return result.value.decision === 'allow'; } describe('PKITS harness', () => { const pkitsCases: readonly PkitsCase[] = PKITS_CASES; const casesBySection = new Map(); for (const pkitsCase of pkitsCases) { const sectionCases = casesBySection.get(pkitsCase.section); if (sectionCases === undefined) { casesBySection.set(pkitsCase.section, [pkitsCase]); continue; } sectionCases.push(pkitsCase); } for (const [section, sectionCases] of casesBySection) { describe(section, () => { for (const pkitsCase of sectionCases) { const register = UNSUPPORTED_ALGORITHM_TESTS.has(pkitsCase.testNumber) ? it.failing : it; register(`${pkitsCase.testNumber} ${pkitsCase.title}`, () => runPkitsCase(pkitsCase)); } }); } }); async function runPkitsCase(pkitsCase: PkitsCase): Promise { const leafName = pkitsCase.certs[pkitsCase.certs.length - 1]; const rootName = pkitsCase.certs[0]; if (leafName === undefined || rootName === undefined) { throw new Error(`PKITS case ${pkitsCase.testNumber} is missing leaf or root`); } const [leaf, root, intermediates] = await Promise.all([ readPkitsCertificateDer(leafName), readPkitsCertificateDer(rootName), Promise.all(pkitsCase.certs.slice(1, -1).map(readPkitsCertificateDer)), ]); const verifyResult = await verifyCertificateChain({ leaf, intermediates, roots: [root], at: PKITS_VALIDATION_TIME, ...(pkitsCase.initialPolicySet === undefined ? {} : { initialPolicySet: pkitsCase.initialPolicySet }), ...(pkitsCase.requireExplicitPolicy === undefined ? {} : { requireExplicitPolicy: pkitsCase.requireExplicitPolicy }), ...(pkitsCase.inhibitPolicyMapping === undefined ? {} : { inhibitPolicyMapping: pkitsCase.inhibitPolicyMapping }), ...(pkitsCase.inhibitAnyPolicy === undefined ? {} : { inhibitAnyPolicy: pkitsCase.inhibitAnyPolicy }), }); if (!shouldEvaluateRevocation(pkitsCase) || !verifyResult.ok) { expect(verifyResult.ok).toBe(pkitsCase.shouldValidate); return; } const revocationResult = pkitsCase.crls.length > 0 ? await evaluatePkitsRevocation(verifyResult.value.chain, pkitsCase.crls) : true; expect(revocationResult).toBe(pkitsCase.shouldValidate); }