feat(relay): rendezvous-relay service — 7 packages + plans (contracts/transport/agent/control-plane/e2e/auth/web)
Multi-tenant reverse-tunnel service ("ngrok for Claude Code" with E2E): a
host-agent dials OUT to an operator-run relay; external devices reach the host
THROUGH the relay, routed by per-tenant subdomain, forwarding ciphertext only
(the relay never sees plaintext). Lets a customer reach their own self-hosted
web-terminal from anywhere with zero networking setup.
Packages — all tsc-strict + vitest green (656 tests), cross-package integration verified:
- relay-contracts: frozen shared contracts (mux frame codec, data model,
capability token, E2E envelope, pairing) — the src/types.ts analog
- term-relay: native WS mux + stateless data plane (subdomain routing, ciphertext forward)
- agent: host-agent (pairing, per-host Ed25519 + mTLS dial-out, forwards to 127.0.0.1:3000)
- control-plane: accounts/hosts registry, pairing-code flow, routing table, provisioning
- relay-e2e: browser<->agent E2E (X25519 ECDH through relay, AEAD, anti-replay, recoverable replay key)
- relay-auth: Passkey/WebAuthn, capability tokens, per-host certs, deny-by-default tenant isolation
- relay-web: browser login + Web Crypto E2E + client-side preview rendering
Security invariants INV1-15 enforced; cross-tenant isolation CI tripwire live
(.github/workflows/relay-tripwire.yml). Design + implementation-level plans in
docs/PLAN_RELAY_*.md and docs/EXPLORE_RELAY_SERVICE.md.
NOTE: generated autonomously per the reviewed plans. The security-critical
packages (relay-e2e, relay-auth) REQUIRE expert security audit before any real
deployment — passing tests prove self-consistency, not resistance to attackers.
Base app (src/, public/) unchanged; concurrent desktop work left uncommitted.
This commit is contained in:
75
control-plane/src/boot/ca-wiring.ts
Normal file
75
control-plane/src/boot/ca-wiring.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* T15 — CA-key / secret-manager bootstrap + KMS-signer factory (§3.1). The intermediate
|
||||
* leaf-signing key is a non-exportable KMS/HSM key: signing is a `sign()` CALL, the raw private
|
||||
* key is NEVER loaded into control-plane memory. `buildCaSigner` FAILS FAST at startup (INV9) if
|
||||
* the key ref is unresolvable OR its policy does not restrict `sign` to the control-plane
|
||||
* service principal. Both T8 `signHostLeaf` and T15 `renewHostLeaf` call this shared primitive —
|
||||
* neither loads a raw key.
|
||||
*/
|
||||
import { createPublicKey } from 'node:crypto'
|
||||
import type { ControlPlaneEnv } from '../env.js'
|
||||
import { ed25519Sign, generateEd25519, type KeyObject } from '../util/crypto.js'
|
||||
|
||||
/** Wraps KMS `sign()`; no raw key ever crosses this boundary. */
|
||||
export interface CaSigner {
|
||||
sign(tbsCert: Uint8Array): Promise<Uint8Array>
|
||||
/** Public verifying key of the CA (for tests / chain assembly). */
|
||||
readonly publicKeyRaw: Uint8Array
|
||||
}
|
||||
|
||||
/**
|
||||
* KMS resolver seam. Production resolves a real KMS/HSM key handle + policy; tests inject a fake.
|
||||
* A resolver returns `{ canSign, policyRestrictedToServicePrincipal }` for the given ref, or throws
|
||||
* if the ref cannot be resolved at all.
|
||||
*/
|
||||
export interface KmsResolver {
|
||||
resolve(keyRef: string): Promise<{
|
||||
readonly signer: CaSigner
|
||||
readonly policyRestrictedToServicePrincipal: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the CA signer for the configured intermediate KMS key. Fail-fast (INV9, §3.1):
|
||||
* - throws if the resolver cannot resolve the ref;
|
||||
* - throws if the KMS key policy is broader than the control-plane service principal.
|
||||
*/
|
||||
export async function buildCaSigner(env: ControlPlaneEnv, resolver: KmsResolver): Promise<CaSigner> {
|
||||
let resolved
|
||||
try {
|
||||
resolved = await resolver.resolve(env.caIntermediateKmsKeyRef)
|
||||
} catch (err: unknown) {
|
||||
// Never log the ref VALUE — only that resolution failed (INV9).
|
||||
throw new Error(
|
||||
`CA intermediate KMS key could not be resolved: ${err instanceof Error ? err.message : 'unknown'}`,
|
||||
)
|
||||
}
|
||||
if (!resolved.policyRestrictedToServicePrincipal) {
|
||||
throw new Error(
|
||||
'CA intermediate KMS key policy is broader than the control-plane service principal (§3.1) — refusing to boot',
|
||||
)
|
||||
}
|
||||
return resolved.signer
|
||||
}
|
||||
|
||||
/**
|
||||
* In-process Ed25519 CA signer — TEST/DEV ONLY (clearly NOT a KMS; the plan mandates a real
|
||||
* non-exportable KMS key in production, §3.1). Exposes the same `sign()` surface so callers are
|
||||
* KMS-shaped and never touch a raw key path themselves.
|
||||
*/
|
||||
export function inProcessCaSigner(privateKey?: KeyObject): CaSigner {
|
||||
const key = privateKey ?? generateEd25519().privateKey
|
||||
const pub = derivePublicRaw(key)
|
||||
return {
|
||||
publicKeyRaw: pub,
|
||||
async sign(tbsCert) {
|
||||
return ed25519Sign(key, tbsCert)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function derivePublicRaw(privateKey: KeyObject): Uint8Array {
|
||||
// Recover the raw 32-byte Ed25519 public key from the private KeyObject.
|
||||
const spki = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }) as Buffer
|
||||
return new Uint8Array(spki.subarray(spki.length - 32))
|
||||
}
|
||||
Reference in New Issue
Block a user