Files
2026-07-13 13:39:12 +08:00

68 lines
3.3 KiB
TypeScript

/**
* Fase 3 / Epic A — dynamic per-SNI certificate authority for the TPROXY capture
* mode. The legacy MITM cert is a single static self-signed cert (DNS-spoof of
* known hosts); TPROXY intercepts ARBITRARY hosts, so it needs a local CA that
* issues a leaf cert per SNI hostname on demand (cached). Built on `selfsigned`
* (which supports CA-signing via `options.ca`), parsed/asserted with Node's
* built-in `crypto.X509Certificate`.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { X509Certificate } from "node:crypto";
const { generateMitmCa, issueLeafCert, DynamicCertStore } = await import(
"../../src/mitm/tproxy/dynamicCert.ts"
);
test("generateMitmCa produces a CA certificate (basicConstraints CA, key+cert PEM)", async () => {
const ca = await generateMitmCa("OmniRoute MITM CA (test)");
assert.match(ca.key, /-----BEGIN (RSA )?PRIVATE KEY-----/);
assert.match(ca.cert, /-----BEGIN CERTIFICATE-----/);
const x = new X509Certificate(ca.cert);
assert.equal(x.ca, true, "CA cert must have basicConstraints CA:TRUE");
assert.match(x.subject, /OmniRoute MITM CA \(test\)/);
});
test("issueLeafCert issues a host leaf signed by the CA, with SAN + chain", async () => {
const ca = await generateMitmCa("OmniRoute MITM CA (test)");
const leaf = await issueLeafCert("api.stripe.com", ca);
assert.match(leaf.key, /PRIVATE KEY-----/);
// cert bundle = leaf + CA chain so clients can build the path
const certs = leaf.cert.match(/-----BEGIN CERTIFICATE-----/g) ?? [];
assert.ok(certs.length >= 2, "leaf bundle should include the leaf + CA cert");
const leafX = new X509Certificate(leaf.cert);
assert.equal(leafX.ca, false, "leaf must not be a CA");
assert.match(leafX.subjectAltName ?? "", /api\.stripe\.com/);
// signed by the CA → issuer matches CA subject
const caX = new X509Certificate(ca.cert);
assert.equal(leafX.issuer, caX.subject, "leaf issuer must equal CA subject");
assert.ok(leafX.verify(caX.publicKey), "leaf signature must verify against the CA public key");
});
test("DynamicCertStore caches one SecureContext per hostname", async () => {
const store = new DynamicCertStore("OmniRoute MITM CA (test)");
const a1 = await store.getSecureContext("a.example.com");
const a2 = await store.getSecureContext("a.example.com");
const b1 = await store.getSecureContext("b.example.com");
assert.equal(a1, a2, "same host → cached SecureContext instance");
assert.notEqual(a1, b1, "different host → different SecureContext");
assert.equal(store.size, 2, "two distinct hosts cached");
});
test("DynamicCertStore.createSNICallback resolves a context for the SNI host", async () => {
const store = new DynamicCertStore("OmniRoute MITM CA (test)");
const cb = store.createSNICallback();
const ctx = await new Promise((resolve, reject) =>
cb("dynamic.example.org", (err: Error | null, c: unknown) => (err ? reject(err) : resolve(c)))
);
assert.ok(ctx, "SNICallback must yield a SecureContext");
});
test("the CA exposes its cert PEM so it can be installed in the trust store", async () => {
const store = new DynamicCertStore("OmniRoute MITM CA (test)");
const caPem = await store.getCaCertPem();
assert.match(caPem, /-----BEGIN CERTIFICATE-----/);
assert.equal(new X509Certificate(caPem).ca, true);
});