/** * B2 · registry-backed MtlsVerifier (INV4/INV14) — replaces the Phase-0 stub in `relay-world.ts:149` * that trusted ANY peer cert. Wraps relay-auth's `verifyAgentCert` against the SHARED host registry * (the same `HostRegistryPort` B1 builds over Postgres) and the pinned agent-CA bundle, returning * `{ hostId, accountId }` ONLY for an enrolled, unrevoked host whose leaf chains to our CA and is in * its validity window. Every other outcome is `null` (fail-closed). * * INV3: `hostId`/`accountId` originate ONLY from the authenticated cert material (SPIFFE-ID) matched * against the registry — never from a client-supplied field. INV14: the pubkey is bound to the * registry AFTER CA-chain validation (a cert can chain to our CA yet still be denied if not enrolled). * * ── ASYNC IMPEDANCE (flagged for the orchestrator; adaptation per task B2) ────────────────────── * The term-relay `MtlsVerifier.verifyPeer` (agent-listener.ts:16) is declared SYNC and its caller * (agent-listener.ts:93) does NOT await it (`if (verified === null)`). A registry-backed verifier * CANNOT be sync — `HostRegistryPort.getById` returns a Promise, so `verifyAgentCert` is async. This * factory therefore returns an `AsyncMtlsVerifier` (Promise-returning `verifyPeer`). Wiring it into the * data-plane requires `MtlsVerifier.verifyPeer` to become async END-TO-END: `agent-listener.ts` must * `await` the result, and the `relay-world.ts:149` stub must be replaced. Those files are OUTSIDE B2's * Owns. Until that migration lands, assigning this into the sync slot type-errors at the wiring site * (`Promise<…>` is not assignable to `{…}|null`) — a useful compile-time tripwire, not a silent bug. */ import { verifyAgentCert, defaultParseX509, type ParseCert, type HostRegistryPort, } from 'relay-auth' const PEM_LINE_WIDTH = 64 /** Non-global (safe for `.test()`): asserts a bundle actually holds a PEM CERTIFICATE block. */ const PEM_CERT_RE = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/ /** * Async analog of term-relay's sync `MtlsVerifier` (agent-listener.ts). See ASYNC IMPEDANCE above: * a registry lookup is inherently async, so `verifyPeer` returns a Promise. */ export interface AsyncMtlsVerifier { verifyPeer(peerCertDer: Uint8Array): Promise<{ hostId: string; accountId: string } | null> } export interface MtlsVerifierDeps { /** Pinned agent-CA bundle (PEM: intermediate(s) + root). NEVER the browser LE chain (INV14). */ readonly caChainPem: string /** Shared host registry (same port B1 builds over Postgres). accountId/hostId only from here (INV3). */ readonly hosts: HostRegistryPort /** Epoch-SECONDS provider (compared against the leaf's notBefore/notAfter). */ readonly now: () => number /** Optional observability seam for a registry/parse fault; the verifier still fails closed. */ readonly onError?: (e: unknown) => void /** Test seam: cert parser (defaults to production `defaultParseX509`). */ readonly parse?: ParseCert } /** * Wrap raw DER bytes (`getPeerCertificate(true).raw`) into a PEM certificate block with 64-column * base64, the shape node's `X509Certificate` (via `verifyAgentCert`) parses. */ export function derToPem(der: Uint8Array): string { const b64 = Buffer.from(der).toString('base64') const wrapped = b64.match(new RegExp(`.{1,${PEM_LINE_WIDTH}}`, 'g'))?.join('\n') ?? '' return `-----BEGIN CERTIFICATE-----\n${wrapped}\n-----END CERTIFICATE-----\n` } export function createMtlsVerifier(deps: MtlsVerifierDeps): AsyncMtlsVerifier { const { caChainPem, hosts, now } = deps const onError = deps.onError ?? (() => {}) const parse = deps.parse ?? defaultParseX509 // Fail fast at construction on a misconfigured CA bundle (INV14: a real pinned CA is required; an // empty/garbage bundle would otherwise silently deny every agent with an opaque `chain_invalid`). if (typeof caChainPem !== 'string' || !PEM_CERT_RE.test(caChainPem)) { throw new Error('createMtlsVerifier: caChainPem must contain at least one PEM CERTIFICATE block') } return { async verifyPeer(peerCertDer) { if (peerCertDer === undefined || peerCertDer === null || peerCertDer.length === 0) { return null // no client cert presented → fail closed } try { const leafPem = derToPem(peerCertDer) const result = await verifyAgentCert(leafPem, caChainPem, now(), hosts, parse) if (!result.ok || result.hostId === undefined || result.accountId === undefined) { return null // expired / chain-invalid / not-enrolled / revoked / account-mismatch → fail closed } return { hostId: result.hostId, accountId: result.accountId } } catch (e: unknown) { onError(e) // registry/DB fault: surface it, never throw out of verifyPeer → fail closed return null } }, } }