/** * B2 tests — registry-backed MtlsVerifier (INV4/INV14, fail-closed). * * Two layers, mirroring relay-auth/test/mtls.test.ts: * 1. REAL X.509 path: a self-signed root → leaf chain (relay-auth's committed mTLS fixtures) fed to * `verifyPeer` as DER bytes, exercising the production `defaultParseX509` chain walk + DER→PEM * conversion end-to-end against a fake host registry. * 2. Deterministic seam: an injected `ParseCert` isolates the DER→PEM conversion + registry-gating + * fail-closed mapping without depending on fixture contents. */ import { readFileSync } from 'node:fs' import { X509Certificate } from 'node:crypto' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import { describe, it, expect } from 'vitest' import { defaultParseX509, spiffeIdFor, type ParseCert, type ParsedCert } from 'relay-auth' import type { HostRegistryPort } from 'relay-auth' import { createMtlsVerifier, derToPem } from '../../src/wiring/mtls-verifier.js' import { fakeHostRegistry, makeHostRecord } from '../../src/wiring/memory-stores.js' // Reuse the committed real mTLS fixtures (leaf chains to ca-chain; SPIFFE account/acct-A/host/host-1). const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), '../../../relay-auth/test/fixtures/mtls') const readFixture = (name: string): string => readFileSync(join(FIXTURES, name), 'utf8') const leafPem = readFixture('leaf.pem') const caChainPem = readFixture('ca-chain.pem') const foreignLeafPem = readFixture('foreign-leaf.pem') const leafDer = new Uint8Array(new X509Certificate(leafPem).raw) const foreignDer = new Uint8Array(new X509Certificate(foreignLeafPem).raw) // A time strictly inside the leaf's validity window (fixtures are long-lived; do not hardcode). const parsedLeaf = defaultParseX509(leafPem, caChainPem) const IN_WINDOW = parsedLeaf.notBefore + 60 const enrolledHosts = (): HostRegistryPort => fakeHostRegistry([makeHostRecord('acct-A', 'host-1', 'sub-host-1')]) describe('derToPem (DER → PEM)', () => { it('wraps DER as a 64-column PEM CERTIFICATE block that round-trips back to the same DER', () => { const pem = derToPem(leafDer) expect(pem.startsWith('-----BEGIN CERTIFICATE-----\n')).toBe(true) expect(pem.trimEnd().endsWith('-----END CERTIFICATE-----')).toBe(true) // No base64 body line exceeds the PEM 64-column width. const body = pem.split('\n').slice(1, -2) for (const line of body) expect(line.length).toBeLessThanOrEqual(64) // Parsing the produced PEM yields byte-identical DER. expect(new Uint8Array(new X509Certificate(pem).raw)).toEqual(leafDer) }) }) describe('createMtlsVerifier — real X.509 path (INV14)', () => { it('accepts an enrolled, in-date leaf chaining to the pinned CA → {hostId, accountId}', async () => { const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW }) expect(await v.verifyPeer(leafDer)).toEqual({ hostId: 'host-1', accountId: 'acct-A' }) }) it('refuses a leaf signed by a foreign CA not in the pinned bundle → null', async () => { const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW }) expect(await v.verifyPeer(foreignDer)).toBeNull() }) it('refuses a valid leaf whose host is NOT in the registry (INV4) → null', async () => { const v = createMtlsVerifier({ caChainPem, hosts: fakeHostRegistry([]), now: () => IN_WINDOW }) expect(await v.verifyPeer(leafDer)).toBeNull() }) it('refuses when the registry account ≠ the cert SPIFFE account → null', async () => { const hosts = fakeHostRegistry([makeHostRecord('acct-B', 'host-1', 'sub-host-1')]) const v = createMtlsVerifier({ caChainPem, hosts, now: () => IN_WINDOW }) expect(await v.verifyPeer(leafDer)).toBeNull() }) it('refuses a revoked host (INV12/registry gate) → null', async () => { const hosts = fakeHostRegistry([makeHostRecord('acct-A', 'host-1', 'sub-host-1', 'revoked')]) const v = createMtlsVerifier({ caChainPem, hosts, now: () => IN_WINDOW }) expect(await v.verifyPeer(leafDer)).toBeNull() }) it('refuses an expired leaf → null', async () => { const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => parsedLeaf.notAfter + 100 }) expect(await v.verifyPeer(leafDer)).toBeNull() }) it('refuses a not-yet-valid leaf → null', async () => { const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => parsedLeaf.notBefore - 100 }) expect(await v.verifyPeer(leafDer)).toBeNull() }) }) describe('createMtlsVerifier — fail-closed on malformed / absent input', () => { it('returns null for an empty DER (no client cert presented)', async () => { const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW }) expect(await v.verifyPeer(new Uint8Array(0))).toBeNull() }) it('returns null for garbage DER bytes that are not a certificate', async () => { const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => IN_WINDOW }) expect(await v.verifyPeer(new Uint8Array([1, 2, 3, 4, 5]))).toBeNull() }) }) describe('createMtlsVerifier — construction validation (INV14)', () => { it('throws when the CA bundle is empty', () => { expect(() => createMtlsVerifier({ caChainPem: '', hosts: enrolledHosts(), now: () => IN_WINDOW })).toThrow() }) it('throws when the CA bundle has no PEM CERTIFICATE block', () => { expect(() => createMtlsVerifier({ caChainPem: 'not a certificate', hosts: enrolledHosts(), now: () => IN_WINDOW }), ).toThrow() }) }) // ── Deterministic seam: isolate DER→PEM + registry gating from fixture contents ────────────────── const okParsed = (spiffeUri: string): ParsedCert => ({ spiffeUri, notBefore: 0, notAfter: 4_000_000_000, chainValid: true, }) describe('createMtlsVerifier — ParseCert seam', () => { it('feeds verifyAgentCert the exact PEM produced by derToPem from the input DER', async () => { let seenLeafPem: string | null = null const capturingParse: ParseCert = (leaf) => { seenLeafPem = leaf return okParsed(spiffeIdFor('acct-A', 'host-1', 'example.com')) } const der = new Uint8Array([9, 8, 7, 6, 5, 4, 3, 2, 1]) const v = createMtlsVerifier({ caChainPem, hosts: enrolledHosts(), now: () => 1_000_000, parse: capturingParse, }) expect(await v.verifyPeer(der)).toEqual({ hostId: 'host-1', accountId: 'acct-A' }) expect(seenLeafPem).toBe(derToPem(der)) }) it('fails closed (null) and reports to onError when the registry lookup throws', async () => { const errors: unknown[] = [] const throwingHosts: HostRegistryPort = { getById: async () => { throw new Error('registry unavailable') }, } const v = createMtlsVerifier({ caChainPem, hosts: throwingHosts, now: () => 1_000_000, onError: (e) => errors.push(e), parse: () => okParsed(spiffeIdFor('acct-A', 'host-1', 'example.com')), }) expect(await v.verifyPeer(new Uint8Array([1, 2, 3]))).toBeNull() expect(errors).toHaveLength(1) }) })