Download src/proxy-capture/ca.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 9.03 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/proxy-capture/ca.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/proxy-capture/ca.ts
-
curl -L -o ca.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/proxy-capture/ca.ts
9.03 kB
| // Proxy capture CA helpers create and inspect local capture CA certificates. | |
| import { createHash, createPrivateKey, randomBytes, X509Certificate } from "node:crypto"; | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { parseCanonicalIpAddress } from "@openclaw/net-policy/ip"; | |
| import { type FileLockOptions, withFileLock } from "../infra/file-lock.js"; | |
| import { resolveSystemBin } from "../infra/resolve-system-bin.js"; | |
| import { KeyedAsyncQueue } from "../plugin-sdk/keyed-async-queue.js"; | |
| import { runExec } from "../process/exec.js"; | |
| const DEBUG_PROXY_CA_GENERATION_TIMEOUT_MS = 30_000; | |
| const LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS = 30_000; | |
| const LOCAL_PROXY_DIR_MODE = 0o700; | |
| const LOCAL_PROXY_PRIVATE_KEY_MODE = 0o600; | |
| function buildLocalProxyCaOpenSslConfig(commonName: string): string { | |
| return [ | |
| "[req]", | |
| "distinguished_name = subject", | |
| "prompt = no", | |
| "", | |
| "[subject]", | |
| `CN = ${commonName}`, | |
| "", | |
| "[v3_ca]", | |
| "basicConstraints = critical, CA:TRUE", | |
| "keyUsage = critical, keyCertSign, cRLSign", | |
| "", | |
| ].join("\n"); | |
| } | |
| const DEBUG_PROXY_CA_LOCK_OPTIONS: FileLockOptions = { | |
| retries: { | |
| // About 36s of minimum backoff covers one full 30s OpenSSL deadline. | |
| retries: 80, | |
| factor: 1.3, | |
| minTimeout: 25, | |
| maxTimeout: 500, | |
| randomize: true, | |
| }, | |
| stale: 60_000, | |
| staleRecovery: "remove-if-unchanged", | |
| }; | |
| const debugProxyCaGenerationQueue = new KeyedAsyncQueue(); | |
| function isValidDebugProxyCaPair(certPath: string, keyPath: string): boolean { | |
| try { | |
| const certStat = fs.lstatSync(certPath); | |
| const keyStat = fs.lstatSync(keyPath); | |
| if (!certStat.isFile() || !keyStat.isFile() || certStat.size === 0 || keyStat.size === 0) { | |
| return false; | |
| } | |
| const cert = new X509Certificate(fs.readFileSync(certPath)); | |
| const key = createPrivateKey(fs.readFileSync(keyPath)); | |
| return cert.ca && cert.checkPrivateKey(key); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| function removeStagingDirBestEffort(stagingDir: string): void { | |
| try { | |
| fs.rmSync(stagingDir, { recursive: true, force: true }); | |
| } catch { | |
| // Cleanup failure must not replace a successful publication result. | |
| } | |
| } | |
| type LocalProxyCaOptions = { | |
| commonName: string; | |
| purpose: string; | |
| validityDays: number; | |
| }; | |
| type LocalProxyCaPair = { | |
| certPath: string; | |
| keyPath: string; | |
| }; | |
| async function ensureLocalProxyCa( | |
| certDir: string, | |
| options: LocalProxyCaOptions, | |
| ): Promise<LocalProxyCaPair> { | |
| fs.mkdirSync(certDir, { recursive: true, mode: LOCAL_PROXY_DIR_MODE }); | |
| fs.chmodSync(certDir, LOCAL_PROXY_DIR_MODE); | |
| const certPath = path.join(certDir, "root-ca.pem"); | |
| const keyPath = path.join(certDir, "root-ca-key.pem"); | |
| const canonicalKeyPath = path.join(fs.realpathSync(certDir), "root-ca-key.pem"); | |
| return await debugProxyCaGenerationQueue.enqueue(canonicalKeyPath, async () => | |
| withFileLock(canonicalKeyPath, DEBUG_PROXY_CA_LOCK_OPTIONS, async () => { | |
| if (isValidDebugProxyCaPair(certPath, keyPath)) { | |
| return { certPath, keyPath }; | |
| } | |
| const openssl = resolveSystemBin("openssl"); | |
| if (!openssl) { | |
| throw new Error(`openssl is required to generate ${options.purpose} certificates`); | |
| } | |
| const stagingDir = fs.mkdtempSync(path.join(certDir, ".root-ca-")); | |
| const stagedConfigPath = path.join(stagingDir, "openssl.cnf"); | |
| const stagedCertPath = path.join(stagingDir, "root-ca.pem"); | |
| const stagedKeyPath = path.join(stagingDir, "root-ca-key.pem"); | |
| try { | |
| fs.writeFileSync(stagedConfigPath, buildLocalProxyCaOpenSslConfig(options.commonName), { | |
| mode: LOCAL_PROXY_PRIVATE_KEY_MODE, | |
| }); | |
| await runExec( | |
| openssl, | |
| [ | |
| "req", | |
| "-config", | |
| stagedConfigPath, | |
| "-extensions", | |
| "v3_ca", | |
| "-x509", | |
| "-newkey", | |
| "rsa:2048", | |
| "-sha256", | |
| "-days", | |
| String(options.validityDays), | |
| "-nodes", | |
| "-keyout", | |
| stagedKeyPath, | |
| "-out", | |
| stagedCertPath, | |
| ], | |
| { logOutput: false, timeoutMs: DEBUG_PROXY_CA_GENERATION_TIMEOUT_MS }, | |
| ); | |
| if (!isValidDebugProxyCaPair(stagedCertPath, stagedKeyPath)) { | |
| throw new Error(`openssl generated invalid ${options.purpose} certificate material`); | |
| } | |
| fs.chmodSync(stagedKeyPath, LOCAL_PROXY_PRIVATE_KEY_MODE); | |
| fs.chmodSync(stagedCertPath, 0o644); | |
| // All OpenClaw writers hold this lock. Same-directory renames replace each | |
| // file atomically; validation repairs a pair interrupted between renames. | |
| fs.renameSync(stagedKeyPath, keyPath); | |
| fs.renameSync(stagedCertPath, certPath); | |
| return { certPath, keyPath }; | |
| } finally { | |
| removeStagingDirBestEffort(stagingDir); | |
| } | |
| }), | |
| ); | |
| } | |
| // Ensure a short-lived root CA for local MITM debug proxy runs. Existing certs | |
| // are reused within the cert dir so repeated starts do not prompt regeneration. | |
| export async function ensureDebugProxyCa(certDir: string): Promise<{ | |
| certPath: string; | |
| keyPath: string; | |
| }> { | |
| return await ensureLocalProxyCa(certDir, { | |
| commonName: "OpenClaw Debug Proxy", | |
| purpose: "debug proxy", | |
| validityDays: 7, | |
| }); | |
| } | |
| /** Generates the root CA for one Gateway-lifetime secret egress proxy. */ | |
| export async function ensureSecretEgressProxyCa(certDir: string): Promise<LocalProxyCaPair> { | |
| return await ensureLocalProxyCa(certDir, { | |
| commonName: "OpenClaw Secret Egress Proxy", | |
| purpose: "secret egress proxy", | |
| // Trust is loaded once by subprocesses. Key retention is still limited to | |
| // this Gateway process; certificate expiry must not impose daily restarts. | |
| validityDays: 3650, | |
| }); | |
| } | |
| function isValidLeafPair(params: { certPath: string; keyPath: string; hostname: string }): boolean { | |
| try { | |
| const cert = new X509Certificate(fs.readFileSync(params.certPath)); | |
| const key = createPrivateKey(fs.readFileSync(params.keyPath)); | |
| const hostMatches = parseCanonicalIpAddress(params.hostname) | |
| ? cert.checkIP(params.hostname) === params.hostname | |
| : cert.checkHost(params.hostname) === params.hostname; | |
| return !cert.ca && cert.checkPrivateKey(key) && hostMatches; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async function generateLocalProxyLeafQueued(params: { | |
| certDir: string; | |
| ca: LocalProxyCaPair; | |
| hostname: string; | |
| }): Promise<{ cert: Buffer; key: Buffer }> { | |
| const openssl = resolveSystemBin("openssl"); | |
| if (!openssl) { | |
| throw new Error("openssl is required to generate local proxy certificates"); | |
| } | |
| const leafKeyPath = path.join(params.certDir, "leaf-key.pem"); | |
| if (!fs.existsSync(leafKeyPath)) { | |
| await runExec(openssl, ["genrsa", "-out", leafKeyPath, "2048"], { | |
| logOutput: false, | |
| timeoutMs: LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS, | |
| }); | |
| fs.chmodSync(leafKeyPath, LOCAL_PROXY_PRIVATE_KEY_MODE); | |
| } | |
| const leafId = createHash("sha256").update(params.hostname).digest("hex"); | |
| const stagingDir = fs.mkdtempSync(path.join(params.certDir, `.leaf-${leafId.slice(0, 12)}-`)); | |
| const csrPath = path.join(stagingDir, "leaf.csr"); | |
| const certPath = path.join(stagingDir, "leaf.pem"); | |
| const extPath = path.join(stagingDir, "leaf.ext"); | |
| try { | |
| const sanKind = parseCanonicalIpAddress(params.hostname) ? "IP" : "DNS"; | |
| fs.writeFileSync( | |
| extPath, | |
| `subjectAltName=${sanKind}:${params.hostname}\nextendedKeyUsage=serverAuth\n`, | |
| { mode: LOCAL_PROXY_PRIVATE_KEY_MODE }, | |
| ); | |
| await runExec( | |
| openssl, | |
| ["req", "-new", "-key", leafKeyPath, "-subj", `/CN=${params.hostname}`, "-out", csrPath], | |
| { logOutput: false, timeoutMs: LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS }, | |
| ); | |
| await runExec( | |
| openssl, | |
| [ | |
| "x509", | |
| "-req", | |
| "-in", | |
| csrPath, | |
| "-CA", | |
| params.ca.certPath, | |
| "-CAkey", | |
| params.ca.keyPath, | |
| "-set_serial", | |
| `0x${randomBytes(16).toString("hex")}`, | |
| "-out", | |
| certPath, | |
| "-days", | |
| "1", | |
| "-sha256", | |
| "-extfile", | |
| extPath, | |
| ], | |
| { logOutput: false, timeoutMs: LOCAL_PROXY_CERT_GENERATION_TIMEOUT_MS }, | |
| ); | |
| if (!isValidLeafPair({ certPath, keyPath: leafKeyPath, hostname: params.hostname })) { | |
| throw new Error("openssl generated invalid local proxy leaf certificate material"); | |
| } | |
| return { | |
| cert: fs.readFileSync(certPath), | |
| key: fs.readFileSync(leafKeyPath), | |
| }; | |
| } finally { | |
| removeStagingDirBestEffort(stagingDir); | |
| } | |
| } | |
| /** Mints one on-demand TLS leaf signed by a local proxy CA. */ | |
| export async function generateLocalProxyLeaf(params: { | |
| certDir: string; | |
| ca: LocalProxyCaPair; | |
| hostname: string; | |
| }): Promise<{ cert: Buffer; key: Buffer }> { | |
| const queueKey = path.join(fs.realpathSync(params.certDir), "leaf-key.pem"); | |
| return await debugProxyCaGenerationQueue.enqueue(queueKey, () => | |
| generateLocalProxyLeafQueued(params), | |
| ); | |
| } | |