File size: 4,340 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
 * Fase 3 / Epic A — OS trust-store install for the TPROXY dynamic CA (decrypt 4b/N).
 *
 * The decrypt capture mode issues per-SNI leaves from a dynamic CA (#4173); the
 * intercepted clients must trust that CA. This installs the CA cert into the OS
 * trust store under a DEDICATED slot (`omniroute-tproxy-ca.crt`), separate from
 * the static MITM cert (`omniroute-mitm.crt` in `cert/install.ts`), so the two
 * never clobber each other.
 *
 * Linux-only (TPROXY is Linux-only). Privileged commands run via the controlled
 * `execFileWithPassword` helper — `spawn` with arg arrays, no shell, no string
 * interpolation (Hard Rule #13). It already runs the target directly (no `sudo`)
 * when the process is root, so on the VPS no password is needed. Every effectful
 * seam is injectable so the command sequence is unit-testable without root.
 */
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileWithPassword } from "../systemCommands.ts";

/** Dedicated trust-store filename — distinct from the static MITM cert's slot. */
export const TPROXY_CA_CERT_NAME = "omniroute-tproxy-ca.crt";

/** Trust-store anchor dirs + refresh command, in detection order (Debian first). */
const LINUX_CERT_PATHS: ReadonlyArray<{ dir: string; cmd: string }> = [
  { dir: "/usr/local/share/ca-certificates", cmd: "update-ca-certificates" },
  { dir: "/etc/ca-certificates/trust-source/anchors", cmd: "update-ca-trust" },
  { dir: "/etc/pki/ca-trust/source/anchors", cmd: "update-ca-trust" },
  { dir: "/etc/pki/trust/anchors", cmd: "update-ca-certificates" },
];

export type SudoRunner = (command: string, args: string[], password: string) => Promise<unknown>;

export interface CaTrustDeps {
  /** Run a privileged command (sudo/arg-array; runs direct when root). */
  run: SudoRunner;
  /** Stage the CA PEM to a local file before the privileged copy. */
  writeFile: (filePath: string, data: string) => void;
  /** Remove the staged file (best-effort). */
  rmFile: (filePath: string) => void;
  /** Directory to stage the PEM in. */
  tmpDir: () => string;
  /** Resolve the trust-store anchor dir + refresh command for this distro. */
  certConfig: () => { dir: string; cmd: string };
  /** Host platform (Linux-only gate). */
  platform: () => string;
}

function detectCertConfig(): { dir: string; cmd: string } {
  for (const c of LINUX_CERT_PATHS) {
    if (fs.existsSync(c.dir)) return c;
  }
  return LINUX_CERT_PATHS[0];
}

const realDeps: CaTrustDeps = {
  run: execFileWithPassword,
  writeFile: (filePath, data) => fs.writeFileSync(filePath, data, { mode: 0o644 }),
  rmFile: (filePath) => {
    try {
      fs.unlinkSync(filePath);
    } catch {
      // best-effort cleanup
    }
  },
  tmpDir: () => os.tmpdir(),
  certConfig: detectCertConfig,
  platform: () => process.platform,
};

/**
 * Install the dynamic CA cert (PEM) into the OS trust store under the dedicated
 * TPROXY slot. Stages the PEM to a temp file, then (privileged) copies it into the
 * anchor dir and refreshes the trust store. Throws on non-Linux hosts.
 */
export async function installTproxyCa(
  caPem: string,
  sudoPassword = "",
  deps: Partial<CaTrustDeps> = {}
): Promise<void> {
  const d = { ...realDeps, ...deps };
  if (d.platform() !== "linux") {
    throw new Error("TPROXY CA trust install is Linux-only.");
  }
  const cfg = d.certConfig();
  const staged = path.join(d.tmpDir(), TPROXY_CA_CERT_NAME);
  const dest = `${cfg.dir}/${TPROXY_CA_CERT_NAME}`;
  d.writeFile(staged, caPem);
  try {
    await d.run("sudo", ["-S", "mkdir", "-p", cfg.dir], sudoPassword);
    await d.run("sudo", ["-S", "cp", staged, dest], sudoPassword);
    await d.run("sudo", ["-S", cfg.cmd], sudoPassword);
  } finally {
    d.rmFile(staged);
  }
}

/**
 * Remove the TPROXY CA from the OS trust store (its dedicated slot only — leaves
 * the static MITM cert untouched) and refresh. No-op on non-Linux hosts.
 */
export async function uninstallTproxyCa(
  sudoPassword = "",
  deps: Partial<CaTrustDeps> = {}
): Promise<void> {
  const d = { ...realDeps, ...deps };
  if (d.platform() !== "linux") return;
  const cfg = d.certConfig();
  const dest = `${cfg.dir}/${TPROXY_CA_CERT_NAME}`;
  await d.run("sudo", ["-S", "rm", "-f", dest], sudoPassword);
  await d.run("sudo", ["-S", cfg.cmd], sudoPassword);
}