68 lines
3.3 KiB
TypeScript
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);
|
|
});
|