import { readFileSync } from 'node:fs'; import { EjbcaProvider } from './ejbca'; import type { CaProvider } from './types'; export * from './types'; /** * Build the configured provider, or `null` when S/MIME enrolment is not set up. * * `null` rather than a throw, so an unconfigured deployment answers 503 on the * enrolment route and is otherwise unaffected. Enrolment is an opt-in feature of * a mail client; a missing CA secret must not stop anyone reading their mail. */ let cached: CaProvider | null | undefined; export function getCaProvider(): CaProvider | null { if (cached !== undefined) return cached; cached = build(); return cached; } function build(): CaProvider | null { const baseUrl = process.env.SMIME_CA_URL; if (!baseUrl) return null; // Read from the mounted secret by default (see deploy/k8s/ca/README.md ยง 5.3). // Paths are overridable for local development against a throwaway CA. const pfxPath = process.env.SMIME_CA_CLIENT_PFX_PATH ?? '/etc/smime-ca/client.p12'; const caPath = process.env.SMIME_CA_CHAIN_PATH ?? '/etc/smime-ca/ca-chain.pem'; const password = process.env.SMIME_CA_CLIENT_PFX_PASSWORD; if (!password) { console.error('[smime-ca] SMIME_CA_URL is set but SMIME_CA_CLIENT_PFX_PASSWORD is not'); return null; } let clientPfx: Buffer; let serverCaPem: string; try { clientPfx = readFileSync(pfxPath); serverCaPem = readFileSync(caPath, 'utf8'); } catch (cause) { // Loud, because the symptom otherwise is "enrolment returns 503" with no // indication that a file is simply not mounted. console.error(`[smime-ca] cannot read RA credential (${pfxPath} / ${caPath}):`, cause); return null; } return new EjbcaProvider({ id: process.env.SMIME_CA_ID ?? 'ejbca', baseUrl: baseUrl.replace(/\/$/, ''), clientPfx, clientPfxPassword: password, serverCaPem, caName: process.env.SMIME_CA_NAME ?? 'VNC S/MIME Issuing CA Sandbox R1', certificateProfile: process.env.SMIME_CA_CERT_PROFILE ?? 'VNC S/MIME 1y', endEntityProfile: process.env.SMIME_CA_EE_PROFILE ?? 'VNC S/MIME User', }); }