import { execFile } from "node:child_process"; import { X509Certificate } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import tls from "node:tls"; import { promisify } from "node:util"; import type { GatewayTlsConfig } from "../../config/types.gateway.js"; import { CONFIG_DIR, ensureDir, resolveUserPath, shortenHomeInString } from "../../utils.js"; const execFileAsync = promisify(execFile); export type GatewayTlsRuntime = { enabled: boolean; required: boolean; certPath?: string; keyPath?: string; caPath?: string; fingerprintSha256?: string; tlsOptions?: tls.TlsOptions; error?: string; }; function normalizeFingerprint(input: string): string { return input.replace(/[^a-fA-F0-9]/g, "").toLowerCase(); } async function fileExists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } } async function generateSelfSignedCert(params: { certPath: string; keyPath: string; log?: { info?: (msg: string) => void }; }): Promise { const certDir = path.dirname(params.certPath); const keyDir = path.dirname(params.keyPath); await ensureDir(certDir); if (keyDir !== certDir) { await ensureDir(keyDir); } await execFileAsync("openssl", [ "req", "-x509", "-newkey", "rsa:2048", "-sha256", "-days", "3650", "-nodes", "-keyout", params.keyPath, "-out", params.certPath, "-subj", "/CN=clawdbot-gateway", ]); await fs.chmod(params.keyPath, 0o600).catch(() => {}); await fs.chmod(params.certPath, 0o600).catch(() => {}); params.log?.info?.( `gateway tls: generated self-signed cert at ${shortenHomeInString(params.certPath)}`, ); } export async function loadGatewayTlsRuntime( cfg: GatewayTlsConfig | undefined, log?: { info?: (msg: string) => void; warn?: (msg: string) => void }, ): Promise { if (!cfg || cfg.enabled !== true) return { enabled: false, required: false }; const autoGenerate = cfg.autoGenerate !== false; const baseDir = path.join(CONFIG_DIR, "gateway", "tls"); const certPath = resolveUserPath(cfg.certPath ?? path.join(baseDir, "gateway-cert.pem")); const keyPath = resolveUserPath(cfg.keyPath ?? path.join(baseDir, "gateway-key.pem")); const caPath = cfg.caPath ? resolveUserPath(cfg.caPath) : undefined; const hasCert = await fileExists(certPath); const hasKey = await fileExists(keyPath); if (!hasCert && !hasKey && autoGenerate) { try { await generateSelfSignedCert({ certPath, keyPath, log }); } catch (err) { return { enabled: false, required: true, certPath, keyPath, error: `gateway tls: failed to generate cert (${String(err)})`, }; } } if (!(await fileExists(certPath)) || !(await fileExists(keyPath))) { return { enabled: false, required: true, certPath, keyPath, error: "gateway tls: cert/key missing", }; } try { const cert = await fs.readFile(certPath, "utf8"); const key = await fs.readFile(keyPath, "utf8"); const ca = caPath ? await fs.readFile(caPath, "utf8") : undefined; const x509 = new X509Certificate(cert); const fingerprintSha256 = normalizeFingerprint(x509.fingerprint256 ?? ""); if (!fingerprintSha256) { return { enabled: false, required: true, certPath, keyPath, caPath, error: "gateway tls: unable to compute certificate fingerprint", }; } return { enabled: true, required: true, certPath, keyPath, caPath, fingerprintSha256, tlsOptions: { cert, key, ca, minVersion: "TLSv1.2", }, }; } catch (err) { return { enabled: false, required: true, certPath, keyPath, caPath, error: `gateway tls: failed to load cert (${String(err)})`, }; } }