/** * Dev-only self-signed TLS material, generated at boot via the system `openssl`. NEVER for * production: a throwaway CA + a browser server cert + an agent-facing server cert signed by that * CA. Private keys are written 0600 and never logged (INV9). The dev CA must never be reused * anywhere real. */ import { execFileSync } from 'node:child_process' import { mkdtempSync, readFileSync, chmodSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' export interface DevCerts { readonly dir: string readonly browserCertPath: string readonly browserKeyPath: string readonly agentServerCertPath: string readonly agentServerKeyPath: string readonly caCertPath: string readonly caCertPem: string cleanup(): void } function openssl(args: readonly string[], cwd: string): void { execFileSync('openssl', args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] }) } /** * Generate the dev PKI. Uses Ed25519 keys (OpenSSL 3). Throws if `openssl` is unavailable or a * step fails — the caller fails fast rather than booting without TLS. */ export function generateDevCerts(browserCn: string): DevCerts { const dir = mkdtempSync(join(tmpdir(), 'relay-run-certs-')) const p = (name: string): string => join(dir, name) // Throwaway CA (agent mTLS trust anchor). openssl( ['req', '-x509', '-newkey', 'ed25519', '-keyout', 'ca.key', '-out', 'ca.crt', '-days', '7', '-nodes', '-subj', '/CN=relay-run-dev-ca'], dir, ) // Browser-facing server cert (self-signed; the user accepts it once). openssl( ['req', '-x509', '-newkey', 'ed25519', '-keyout', 'browser.key', '-out', 'browser.crt', '-days', '7', '-nodes', '-subj', `/CN=${browserCn}`, '-addext', `subjectAltName=DNS:${browserCn},DNS:localhost,IP:127.0.0.1`], dir, ) // Agent-facing server cert, signed by the dev CA (the agent pins this CA). openssl( ['req', '-new', '-newkey', 'ed25519', '-keyout', 'agent-server.key', '-out', 'agent-server.csr', '-nodes', '-subj', '/CN=localhost', '-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1'], dir, ) openssl( ['x509', '-req', '-in', 'agent-server.csr', '-CA', 'ca.crt', '-CAkey', 'ca.key', '-CAcreateserial', '-out', 'agent-server.crt', '-days', '7', '-copy_extensions', 'copy'], dir, ) for (const key of ['ca.key', 'browser.key', 'agent-server.key']) chmodSync(p(key), 0o600) return { dir, browserCertPath: p('browser.crt'), browserKeyPath: p('browser.key'), agentServerCertPath: p('agent-server.crt'), agentServerKeyPath: p('agent-server.key'), caCertPath: p('ca.crt'), caCertPem: readFileSync(p('ca.crt'), 'utf8'), cleanup() { rmSync(dir, { recursive: true, force: true }) }, } }