feat(relay-run): Phase 0 single-host relay — real data-plane wiring + integration test
New relay-run/ package boots the REAL term-relay relay-node between a browser WSS
listener and an agent mTLS listener, delegating every authz verdict to the real
relay-auth onUpgrade (Origin/CSWSH + capability verify + DPoP PoP + single-use jti),
with the real P4 E2E crypto. No audited package modified.
- P1 (done): in-process integration test round-trips a sealed payload both ways
through the real createRelayNode splice + real createMuxSession; INV2 asserted
(relay sees only ciphertext); negative controls (foreign Origin / missing DPoP -> 401).
- P2 (boots): main.ts brings up self-signed TLS + 3 listeners + in-memory control-plane.
- P3 (not landed): no agent dial / node-pty / relay-web serve yet.
- 4 tests green, tsc clean. node_modules via symlinks (gitignored).
Two real seam drifts surfaced (documented in PLAN_RELAY_RUN_PHASE0, not hacked):
(1) term-relay authz-port DpopContext shape vs relay-auth — reconciled (server-derived htu/htm);
(2) control-plane CapabilityVerifier.verify is sync but relay-auth verify is async — blocks
HTTP account/pairing seeding until a 1-line async widen.
This commit is contained in:
77
relay-run/src/certs/dev-certs.ts
Normal file
77
relay-run/src/certs/dev-certs.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Dev-only self-signed TLS material, generated at boot via the system `openssl`. NEVER for
|
||||
* production: a throwaway CA + a browser server cert + an agent-facing server cert signed by that
|
||||
* CA. Private keys are written 0600 and never logged (INV9). The dev CA must never be reused
|
||||
* anywhere real.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdtempSync, readFileSync, chmodSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
export interface DevCerts {
|
||||
readonly dir: string
|
||||
readonly browserCertPath: string
|
||||
readonly browserKeyPath: string
|
||||
readonly agentServerCertPath: string
|
||||
readonly agentServerKeyPath: string
|
||||
readonly caCertPath: string
|
||||
readonly caCertPem: string
|
||||
cleanup(): void
|
||||
}
|
||||
|
||||
function openssl(args: readonly string[], cwd: string): void {
|
||||
execFileSync('openssl', args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the dev PKI. Uses Ed25519 keys (OpenSSL 3). Throws if `openssl` is unavailable or a
|
||||
* step fails — the caller fails fast rather than booting without TLS.
|
||||
*/
|
||||
export function generateDevCerts(browserCn: string): DevCerts {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'relay-run-certs-'))
|
||||
const p = (name: string): string => join(dir, name)
|
||||
|
||||
// Throwaway CA (agent mTLS trust anchor).
|
||||
openssl(
|
||||
['req', '-x509', '-newkey', 'ed25519', '-keyout', 'ca.key', '-out', 'ca.crt', '-days', '7',
|
||||
'-nodes', '-subj', '/CN=relay-run-dev-ca'],
|
||||
dir,
|
||||
)
|
||||
|
||||
// Browser-facing server cert (self-signed; the user accepts it once).
|
||||
openssl(
|
||||
['req', '-x509', '-newkey', 'ed25519', '-keyout', 'browser.key', '-out', 'browser.crt',
|
||||
'-days', '7', '-nodes', '-subj', `/CN=${browserCn}`,
|
||||
'-addext', `subjectAltName=DNS:${browserCn},DNS:localhost,IP:127.0.0.1`],
|
||||
dir,
|
||||
)
|
||||
|
||||
// Agent-facing server cert, signed by the dev CA (the agent pins this CA).
|
||||
openssl(
|
||||
['req', '-new', '-newkey', 'ed25519', '-keyout', 'agent-server.key', '-out', 'agent-server.csr',
|
||||
'-nodes', '-subj', '/CN=localhost',
|
||||
'-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1'],
|
||||
dir,
|
||||
)
|
||||
openssl(
|
||||
['x509', '-req', '-in', 'agent-server.csr', '-CA', 'ca.crt', '-CAkey', 'ca.key',
|
||||
'-CAcreateserial', '-out', 'agent-server.crt', '-days', '7', '-copy_extensions', 'copy'],
|
||||
dir,
|
||||
)
|
||||
|
||||
for (const key of ['ca.key', 'browser.key', 'agent-server.key']) chmodSync(p(key), 0o600)
|
||||
|
||||
return {
|
||||
dir,
|
||||
browserCertPath: p('browser.crt'),
|
||||
browserKeyPath: p('browser.key'),
|
||||
agentServerCertPath: p('agent-server.crt'),
|
||||
agentServerKeyPath: p('agent-server.key'),
|
||||
caCertPath: p('ca.crt'),
|
||||
caCertPem: readFileSync(p('ca.crt'), 'utf8'),
|
||||
cleanup() {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user