open-managed-agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MITM CA loader/generator for the in-process TLS-terminating proxy.
|
|
3
|
+
*
|
|
4
|
+
* The CA is supplied via `network.tlsTerminate.{caCertPath,caKeyPath}` (see
|
|
5
|
+
* sandbox-config.ts). If both paths are omitted, SRT generates an ephemeral
|
|
6
|
+
* RSA-2048 self-signed CA into a temp directory; the cert path is what the
|
|
7
|
+
* trust env vars point at. The caller is responsible for cleaning up via
|
|
8
|
+
* `disposeMitmCA()` (SandboxManager.reset() does this).
|
|
9
|
+
*/
|
|
10
|
+
import forge from 'node-forge';
|
|
11
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { rm } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { rootCertificates } from 'node:tls';
|
|
16
|
+
import { logForDebugging } from "../utils/debug.js";
|
|
17
|
+
const { pki, md, random, util } = forge;
|
|
18
|
+
/**
|
|
19
|
+
* Matches one PEM CERTIFICATE block. Used to extract just the certificates
|
|
20
|
+
* out of files listed in tlsTerminate.extraCaCertPaths before they are
|
|
21
|
+
* copied into the (world-readable) trust bundle.
|
|
22
|
+
*/
|
|
23
|
+
const PEM_CERT_BLOCK = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
|
24
|
+
/**
|
|
25
|
+
* Create a MitmCA. If `caCertPath`/`caKeyPath` are provided, load from disk
|
|
26
|
+
* (throws if either file is missing, unreadable, not PEM, fails to parse, or
|
|
27
|
+
* the key is not RSA). If both are omitted, generate an ephemeral CA into a
|
|
28
|
+
* fresh temp directory.
|
|
29
|
+
*
|
|
30
|
+
* Pure factory: no module-level state. The caller (SandboxManager) owns the
|
|
31
|
+
* returned object and its lifetime.
|
|
32
|
+
*/
|
|
33
|
+
export function createMitmCA(opts) {
|
|
34
|
+
if (opts.caCertPath && opts.caKeyPath) {
|
|
35
|
+
return loadCA(opts.caCertPath, opts.caKeyPath, opts.extraCaCertPaths);
|
|
36
|
+
}
|
|
37
|
+
if (opts.caCertPath || opts.caKeyPath) {
|
|
38
|
+
throw new Error('tlsTerminate: caCertPath and caKeyPath must be provided together');
|
|
39
|
+
}
|
|
40
|
+
return generateEphemeralCA(opts.extraCaCertPaths);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Remove the SRT-owned temp directories for this CA: the trust-bundle dir
|
|
44
|
+
* always, and the cert/key dir too when SRT generated the CA (for an
|
|
45
|
+
* ephemeral CA they are the same directory). User-supplied CA files are
|
|
46
|
+
* left alone.
|
|
47
|
+
*/
|
|
48
|
+
export async function disposeMitmCA(ca) {
|
|
49
|
+
const dirs = new Set([dirname(ca.trustBundlePath)]);
|
|
50
|
+
if (ca.ephemeral)
|
|
51
|
+
dirs.add(dirname(ca.certPath));
|
|
52
|
+
for (const dir of dirs) {
|
|
53
|
+
try {
|
|
54
|
+
await rm(dir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logForDebugging(`[mitm-ca] cleanup failed: ${err.message}`, {
|
|
58
|
+
level: 'warn',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Write the child-facing trust bundle into `dir` and return its path: the
|
|
65
|
+
* MITM CA first, then the roots the host would normally trust, so HTTPS
|
|
66
|
+
* clients in the sandbox accept proxy-minted leaves AND can still verify the
|
|
67
|
+
* real certificate of any host SRT tunnels opaquely instead of terminating
|
|
68
|
+
* (tlsTerminate.excludeDomains). Bundling Node's root store is best-effort
|
|
69
|
+
* compatibility — a tool with its own CA file config is unaffected. Extra
|
|
70
|
+
* roots from tlsTerminate.extraCaCertPaths go last, with the same append
|
|
71
|
+
* semantics as NODE_EXTRA_CA_CERTS.
|
|
72
|
+
*/
|
|
73
|
+
function writeTrustBundle(dir, caCertPem, extraCaCertPaths) {
|
|
74
|
+
const parts = [caCertPem.trim(), ...rootCertificates];
|
|
75
|
+
// Honour extra roots the parent process trusts (e.g. a corporate CA).
|
|
76
|
+
const extra = process.env.NODE_EXTRA_CA_CERTS;
|
|
77
|
+
if (extra) {
|
|
78
|
+
try {
|
|
79
|
+
parts.push(readFileSync(extra, 'utf8').trim());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Missing/unreadable NODE_EXTRA_CA_CERTS: ignore, same as Node does.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Honour site-local roots from config (tlsTerminate.extraCaCertPaths),
|
|
86
|
+
// e.g. an internal mTLS CA presented by excluded/passthrough hosts. Paths
|
|
87
|
+
// may exist on only some hosts, so a missing file is skipped, not fatal —
|
|
88
|
+
// but log it, or a typo'd path is undiagnosable.
|
|
89
|
+
for (const extraPath of extraCaCertPaths ?? []) {
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = readFileSync(extraPath, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const code = err.code ?? String(err);
|
|
96
|
+
logForDebugging(`[mitm-ca] extraCaCertPaths: cannot read ${extraPath} (${code}); ` +
|
|
97
|
+
`skipping`, { level: 'warn' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Append only the CERTIFICATE blocks. The bundle is world-readable
|
|
101
|
+
// (0o644) and handed to the sandboxed child, so anything else the file
|
|
102
|
+
// carries (e.g. the key of a combined cert+key PEM) must not be copied.
|
|
103
|
+
const certs = raw.match(PEM_CERT_BLOCK);
|
|
104
|
+
if (!certs) {
|
|
105
|
+
logForDebugging(`[mitm-ca] extraCaCertPaths: ${extraPath} has no PEM CERTIFICATE ` +
|
|
106
|
+
`block; skipping`, { level: 'warn' });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
parts.push(...certs);
|
|
110
|
+
}
|
|
111
|
+
const path = join(dir, 'trust-bundle.crt');
|
|
112
|
+
writeFileSync(path, parts.join('\n') + '\n', { mode: 0o644 });
|
|
113
|
+
return path;
|
|
114
|
+
}
|
|
115
|
+
function loadCA(certPath, keyPath, extraCaCertPaths) {
|
|
116
|
+
const certPem = readPem(certPath, 'CERTIFICATE', 'tlsTerminate.caCertPath');
|
|
117
|
+
const keyPem = readPem(keyPath, 'PRIVATE KEY', 'tlsTerminate.caKeyPath');
|
|
118
|
+
let cert;
|
|
119
|
+
let key;
|
|
120
|
+
try {
|
|
121
|
+
cert = pki.certificateFromPem(certPem);
|
|
122
|
+
key = pki.privateKeyFromPem(keyPem);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
throw new Error(`tlsTerminate: failed to parse CA from ${certPath}: ` +
|
|
126
|
+
err.message);
|
|
127
|
+
}
|
|
128
|
+
if (!('n' in key) || !('d' in key)) {
|
|
129
|
+
// node-forge can only sign with RSA private keys.
|
|
130
|
+
throw new Error(`tlsTerminate.caKeyPath: CA key at ${keyPath} must be RSA`);
|
|
131
|
+
}
|
|
132
|
+
// The CA files are the user's; the trust bundle still needs an SRT-owned
|
|
133
|
+
// directory of its own.
|
|
134
|
+
const bundleDir = mkdtempSync(join(tmpdir(), 'srt-ca-'));
|
|
135
|
+
const trustBundlePath = writeTrustBundle(bundleDir, certPem, extraCaCertPaths);
|
|
136
|
+
logForDebugging(`[mitm-ca] loaded CA from ${certPath}`);
|
|
137
|
+
return {
|
|
138
|
+
certPath,
|
|
139
|
+
keyPath,
|
|
140
|
+
trustBundlePath,
|
|
141
|
+
certPem,
|
|
142
|
+
keyPem,
|
|
143
|
+
cert,
|
|
144
|
+
key: key,
|
|
145
|
+
leafCerts: new Map(),
|
|
146
|
+
secureContexts: new Map(),
|
|
147
|
+
ephemeral: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function generateEphemeralCA(extraCaCertPaths) {
|
|
151
|
+
const keys = pki.rsa.generateKeyPair(2048);
|
|
152
|
+
const cert = pki.createCertificate();
|
|
153
|
+
cert.publicKey = keys.publicKey;
|
|
154
|
+
cert.serialNumber = randomSerial();
|
|
155
|
+
cert.validity.notBefore = daysFromNow(-1);
|
|
156
|
+
cert.validity.notAfter = daysFromNow(825);
|
|
157
|
+
const subject = [
|
|
158
|
+
{ name: 'commonName', value: 'sandbox-runtime ephemeral CA' },
|
|
159
|
+
{ name: 'organizationName', value: 'sandbox-runtime' },
|
|
160
|
+
];
|
|
161
|
+
cert.setSubject(subject);
|
|
162
|
+
cert.setIssuer(subject);
|
|
163
|
+
cert.setExtensions([
|
|
164
|
+
{ name: 'basicConstraints', cA: true, critical: true },
|
|
165
|
+
{
|
|
166
|
+
name: 'keyUsage',
|
|
167
|
+
critical: true,
|
|
168
|
+
keyCertSign: true,
|
|
169
|
+
cRLSign: true,
|
|
170
|
+
digitalSignature: true,
|
|
171
|
+
},
|
|
172
|
+
{ name: 'subjectKeyIdentifier' },
|
|
173
|
+
]);
|
|
174
|
+
cert.sign(keys.privateKey, md.sha256.create());
|
|
175
|
+
const certPem = pki.certificateToPem(cert);
|
|
176
|
+
const keyPem = pki.privateKeyToPem(keys.privateKey);
|
|
177
|
+
// Write to disk so trust env vars (NODE_EXTRA_CA_CERTS etc.) can point at
|
|
178
|
+
// a real path. mkdtemp gives us an unguessable per-process directory.
|
|
179
|
+
const dir = mkdtempSync(join(tmpdir(), 'srt-ca-'));
|
|
180
|
+
const certPath = join(dir, 'ca.crt');
|
|
181
|
+
const keyPath = join(dir, 'ca.key');
|
|
182
|
+
writeFileSync(certPath, certPem, { mode: 0o644 });
|
|
183
|
+
writeFileSync(keyPath, keyPem, { mode: 0o600 });
|
|
184
|
+
const trustBundlePath = writeTrustBundle(dir, certPem, extraCaCertPaths);
|
|
185
|
+
logForDebugging(`[mitm-ca] generated ephemeral CA at ${certPath}`);
|
|
186
|
+
return {
|
|
187
|
+
certPath,
|
|
188
|
+
keyPath,
|
|
189
|
+
trustBundlePath,
|
|
190
|
+
certPem,
|
|
191
|
+
keyPem,
|
|
192
|
+
cert,
|
|
193
|
+
key: keys.privateKey,
|
|
194
|
+
leafCerts: new Map(),
|
|
195
|
+
secureContexts: new Map(),
|
|
196
|
+
ephemeral: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function readPem(path, label, field) {
|
|
200
|
+
let pem;
|
|
201
|
+
try {
|
|
202
|
+
pem = readFileSync(path, 'utf8');
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
const code = err.code ?? String(err);
|
|
206
|
+
throw new Error(`${field}: cannot read ${path} (${code})`);
|
|
207
|
+
}
|
|
208
|
+
// Accept either the exact label or a prefixed variant (e.g. "RSA PRIVATE KEY",
|
|
209
|
+
// "EC PRIVATE KEY") for the key case.
|
|
210
|
+
if (!new RegExp(`-----BEGIN [A-Z ]*${label}-----`).test(pem)) {
|
|
211
|
+
throw new Error(`${field}: ${path} is not a PEM ${label}`);
|
|
212
|
+
}
|
|
213
|
+
return pem;
|
|
214
|
+
}
|
|
215
|
+
function randomSerial() {
|
|
216
|
+
// 16 random bytes, high bit cleared so the DER INTEGER stays positive.
|
|
217
|
+
const bytes = random.getBytesSync(16);
|
|
218
|
+
const hex = util.bytesToHex(bytes);
|
|
219
|
+
const firstNibble = parseInt(hex[0], 16) & 0x7;
|
|
220
|
+
return firstNibble.toString(16) + hex.slice(1);
|
|
221
|
+
}
|
|
222
|
+
function daysFromNow(days) {
|
|
223
|
+
const d = new Date();
|
|
224
|
+
d.setDate(d.getDate() + days);
|
|
225
|
+
return d;
|
|
226
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-host leaf certificate minter for the in-process TLS-terminating proxy.
|
|
3
|
+
*
|
|
4
|
+
* Given a MitmCA (see mitm-ca.ts), mints a leaf certificate for a specific
|
|
5
|
+
* hostname on first use and caches it for the lifetime of that CA instance.
|
|
6
|
+
* The leaf is signed by the CA and carries SAN=DNS:<host> (or IP:<addr>),
|
|
7
|
+
* so a client that trusts the CA will accept it for that host.
|
|
8
|
+
*/
|
|
9
|
+
import forge from 'node-forge';
|
|
10
|
+
import { isIP } from 'node:net';
|
|
11
|
+
import { createSecureContext } from 'node:tls';
|
|
12
|
+
import { logForDebugging } from "../utils/debug.js";
|
|
13
|
+
const { pki, md, random, util } = forge;
|
|
14
|
+
/**
|
|
15
|
+
* Mint (or return cached) an RSA-2048 leaf cert for `hostname`, signed by `ca`.
|
|
16
|
+
* The cache lives on `ca.leafCerts`.
|
|
17
|
+
*/
|
|
18
|
+
export function mintLeafCert(ca, hostname) {
|
|
19
|
+
const cached = ca.leafCerts.get(hostname);
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
const keys = pki.rsa.generateKeyPair(2048);
|
|
23
|
+
const cert = pki.createCertificate();
|
|
24
|
+
cert.publicKey = keys.publicKey;
|
|
25
|
+
cert.serialNumber = randomSerial();
|
|
26
|
+
const notBefore = daysFromNow(-1);
|
|
27
|
+
cert.validity.notBefore = notBefore;
|
|
28
|
+
cert.validity.notAfter = clampValidity(ca.cert, notBefore);
|
|
29
|
+
cert.setSubject([{ name: 'commonName', value: hostname }]);
|
|
30
|
+
cert.setIssuer(ca.cert.subject.attributes);
|
|
31
|
+
cert.setExtensions([
|
|
32
|
+
{ name: 'basicConstraints', cA: false, critical: true },
|
|
33
|
+
{
|
|
34
|
+
name: 'keyUsage',
|
|
35
|
+
critical: true,
|
|
36
|
+
digitalSignature: true,
|
|
37
|
+
keyEncipherment: true,
|
|
38
|
+
},
|
|
39
|
+
{ name: 'extKeyUsage', serverAuth: true },
|
|
40
|
+
{ name: 'subjectAltName', altNames: [sanFor(hostname)] },
|
|
41
|
+
{ name: 'subjectKeyIdentifier' },
|
|
42
|
+
// Python ≥3.13 enables ssl.VERIFY_X509_STRICT by default, which enforces
|
|
43
|
+
// RFC 5280's "AKI MUST be present on non-self-signed certs". curl/Go/Node
|
|
44
|
+
// don't enforce this, but requests/urllib3/httpx/google-auth all reject
|
|
45
|
+
// a leaf without AKI under 3.13. See caSubjectKeyId() for why we can't
|
|
46
|
+
// just pass the CA's stored SKI value through.
|
|
47
|
+
{ name: 'authorityKeyIdentifier', keyIdentifier: caSubjectKeyId(ca.cert) },
|
|
48
|
+
]);
|
|
49
|
+
cert.sign(ca.key, md.sha256.create());
|
|
50
|
+
const leaf = {
|
|
51
|
+
certPem: pki.certificateToPem(cert) + ca.certPem,
|
|
52
|
+
keyPem: pki.privateKeyToPem(keys.privateKey),
|
|
53
|
+
};
|
|
54
|
+
ca.leafCerts.set(hostname, leaf);
|
|
55
|
+
logForDebugging(`[mitm-leaf] minted RSA leaf for ${hostname}`);
|
|
56
|
+
return leaf;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Mint-or-cache a Node TLS SecureContext for `hostname`. Intended as the
|
|
60
|
+
* SNICallback target in the terminating proxy. The cache lives on
|
|
61
|
+
* `ca.secureContexts`.
|
|
62
|
+
*/
|
|
63
|
+
export function secureContextFor(ca, hostname) {
|
|
64
|
+
const cached = ca.secureContexts.get(hostname);
|
|
65
|
+
if (cached)
|
|
66
|
+
return cached;
|
|
67
|
+
const { certPem, keyPem } = mintLeafCert(ca, hostname);
|
|
68
|
+
const ctx = createSecureContext({ cert: certPem, key: keyPem });
|
|
69
|
+
ca.secureContexts.set(hostname, ctx);
|
|
70
|
+
return ctx;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Return the CA's Subject Key Identifier as raw bytes for use as the leaf's
|
|
74
|
+
* authorityKeyIdentifier.keyIdentifier.
|
|
75
|
+
*
|
|
76
|
+
* node-forge stores a cert's subjectKeyIdentifier extension value as a *hex
|
|
77
|
+
* string* (both for in-memory certs and certs parsed from PEM), but expects
|
|
78
|
+
* AKI's keyIdentifier as *raw bytes* — passing the hex through verbatim
|
|
79
|
+
* encodes the ASCII hex chars as the key id and the chain fails to verify.
|
|
80
|
+
* If the CA has no SKI extension (e.g. a v1 user-supplied CA), derive the
|
|
81
|
+
* RFC 5280 method-1 value from its public key.
|
|
82
|
+
*/
|
|
83
|
+
function caSubjectKeyId(caCert) {
|
|
84
|
+
const ext = caCert.getExtension('subjectKeyIdentifier');
|
|
85
|
+
return ext?.subjectKeyIdentifier
|
|
86
|
+
? util.hexToBytes(ext.subjectKeyIdentifier)
|
|
87
|
+
: caCert.generateSubjectKeyIdentifier().getBytes();
|
|
88
|
+
}
|
|
89
|
+
function sanFor(hostname) {
|
|
90
|
+
// RFC 5280 GeneralName tags: 2 = dNSName, 7 = iPAddress.
|
|
91
|
+
return isIP(hostname) !== 0
|
|
92
|
+
? { type: 7, ip: hostname }
|
|
93
|
+
: { type: 2, value: hostname };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Leaf validity capped at min(CA notAfter, notBefore+99d).
|
|
97
|
+
*
|
|
98
|
+
* 99d sits below every TLS cert validity ceiling we care about: the current
|
|
99
|
+
* (2026-03) CA/B baseline of 200d, the next step-down to 100d in 2027-03, the
|
|
100
|
+
* historical 398d public cap, and the macOS ~825d ceiling that trips
|
|
101
|
+
* errSecCertificateIsNotStandardsCompliant on user-installed roots. Leaves are
|
|
102
|
+
* re-minted per session, so we don't need the headroom.
|
|
103
|
+
*
|
|
104
|
+
* Anchored at notBefore — not now — because notBefore is backdated by 1 day,
|
|
105
|
+
* so `now+Nd` would produce an (N+1)-day span and lose a day of margin.
|
|
106
|
+
*/
|
|
107
|
+
function clampValidity(caCert, notBefore) {
|
|
108
|
+
const caEnd = caCert.validity.notAfter;
|
|
109
|
+
const max = new Date(notBefore);
|
|
110
|
+
max.setDate(max.getDate() + 99);
|
|
111
|
+
return caEnd < max ? new Date(caEnd) : max;
|
|
112
|
+
}
|
|
113
|
+
function randomSerial() {
|
|
114
|
+
// 16 random bytes, high bit cleared so the DER INTEGER stays positive.
|
|
115
|
+
const bytes = random.getBytesSync(16);
|
|
116
|
+
const hex = util.bytesToHex(bytes);
|
|
117
|
+
const firstNibble = parseInt(hex[0], 16) & 0x7;
|
|
118
|
+
return firstNibble.toString(16) + hex.slice(1);
|
|
119
|
+
}
|
|
120
|
+
function daysFromNow(days) {
|
|
121
|
+
const d = new Date();
|
|
122
|
+
d.setDate(d.getDate() + days);
|
|
123
|
+
return d;
|
|
124
|
+
}
|