feat(control-plane): real X.509 agent leaf issuance (closes enrollment↔mTLS gap)
- ca/issue.ts: real X.509 v3 Ed25519 leaf with SPIFFE URI SAN, signed by the intermediate; SAN built via relay-auth spiffeIdFor so it can't drift - ca/csr.ts: parse real PKCS#10 (agent's PEM) + async PoP verify; decodeCsrWire accepts PEM or base64(DER) - main.ts/env.ts: real issuer when CA_INTERMEDIATE_KEY_PATH present, else dev fallback - interop.test.ts: oracle proving relay-auth verifyAgentCert accepts the leaf (6 tests) - deps: @peculiar/x509, reflect-metadata
This commit is contained in:
@@ -12,9 +12,10 @@
|
||||
* Ed25519 signer (DEV ONLY — NOT a real KMS).
|
||||
*/
|
||||
import Fastify, { type FastifyInstance } from 'fastify'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import type { ControlPlaneEnv } from './env.js'
|
||||
import { createMemoryStores } from './store/memory.js'
|
||||
import type { Stores } from './store/ports.js'
|
||||
import type { Stores, HostStore } from './store/ports.js'
|
||||
import { createAuditLog } from './audit/log.js'
|
||||
import { createAccountRegistry } from './registry/accounts.js'
|
||||
import { createHostRegistry } from './registry/hosts.js'
|
||||
@@ -22,14 +23,15 @@ import { createSessionRegistry } from './registry/sessions.js'
|
||||
import { createSubdomainAssigner } from './subdomain/assign.js'
|
||||
import { createPairingIssuer } from './pairing/issue.js'
|
||||
import { createPairingRedeemer } from './pairing/redeem.js'
|
||||
import { createLeafSigner } from './ca/sign.js'
|
||||
import { createLeafSigner, type LeafSigner } from './ca/sign.js'
|
||||
import { loadRealLeafSigner } from './ca/issue.js'
|
||||
import { createRoutingTable } from './routing/table.js'
|
||||
import { createInMemoryRevocationBus, type TestableRevocationBus } from './routing/bus.js'
|
||||
import { createMeteringCollector } from './metering/collect.js'
|
||||
import { createDeprovisioner } from './deprovision/deprovision.js'
|
||||
import { createAuthorizer, type CapabilityVerifier } from './api/authz.js'
|
||||
import { buildRouter } from './api/provision.js'
|
||||
import { buildCaSigner, inProcessCaSigner, type KmsResolver } from './boot/ca-wiring.js'
|
||||
import { buildCaSigner, inProcessCaSigner, type KmsResolver, type CaSigner } from './boot/ca-wiring.js'
|
||||
import { configureCapabilityVerifyKey } from './boot/verifier.js'
|
||||
import type { RevocationBus } from 'relay-contracts'
|
||||
|
||||
@@ -52,6 +54,36 @@ const refuseAllVerifier: CapabilityVerifier = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the leaf signer. When the intermediate PRIVATE key file is present on disk, issue REAL
|
||||
* X.509 leaves (production); when absent, fall back to the dev placeholder signer so tests and
|
||||
* key-less dev boots still run. A present-but-unreadable/malformed key FAILS FAST (INV9).
|
||||
*/
|
||||
async function buildLeafSigner(
|
||||
env: ControlPlaneEnv,
|
||||
hosts: HostStore,
|
||||
caSigner: CaSigner,
|
||||
caChainDer: readonly Uint8Array[],
|
||||
): Promise<LeafSigner> {
|
||||
if (!existsSync(env.caIntermediateKeyPath)) {
|
||||
return createLeafSigner({ hosts, signer: caSigner, caChainDer })
|
||||
}
|
||||
try {
|
||||
return await loadRealLeafSigner({
|
||||
hosts,
|
||||
intermediateKeyPem: readFileSync(env.caIntermediateKeyPath, 'utf8'),
|
||||
intermediateCertPem: readFileSync(env.caIntermediateCertPath, 'utf8'),
|
||||
rootCertPem: readFileSync(env.caRootCertPath, 'utf8'),
|
||||
trustDomain: env.relayTrustDomain,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
// Never echo key material — only that loading failed and which path shape was configured (INV9).
|
||||
throw new Error(
|
||||
`failed to load CA leaf-signing material: ${err instanceof Error ? err.message : 'unknown'}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** In-process KMS resolver — DEV ONLY. Production injects a real non-exportable KMS key (§3.1). */
|
||||
function devKmsResolver(): KmsResolver {
|
||||
const signer = inProcessCaSigner()
|
||||
@@ -77,7 +109,7 @@ export async function buildControlPlane(
|
||||
const subdomains = createSubdomainAssigner({ subdomains: stores.subdomains, audit })
|
||||
|
||||
const caSigner = await buildCaSigner(env, overrides.kmsResolver ?? devKmsResolver())
|
||||
const leafSigner = createLeafSigner({ hosts: stores.hosts, signer: caSigner, caChainDer })
|
||||
const leafSigner = await buildLeafSigner(env, stores.hosts, caSigner, caChainDer)
|
||||
|
||||
const pairingIssuer = createPairingIssuer({ pairing: stores.pairing, pairingTtlSec: env.pairingTtlSec, audit })
|
||||
const redeemer = createPairingRedeemer({
|
||||
|
||||
Reference in New Issue
Block a user