Files
web-terminal/control-plane/test/ca.test.ts
Yaojia Wang 6efed9772e 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
2026-07-06 20:46:01 +02:00

74 lines
4.1 KiB
TypeScript

import { describe, test, expect, vi } from 'vitest'
import { createMemoryStores } from '../src/store/memory.js'
import { createHostRegistry } from '../src/registry/hosts.js'
import { createLeafSigner, LeafSignError } from '../src/ca/sign.js'
import { inProcessCaSigner } from '../src/boot/ca-wiring.js'
import { buildCsr, verifyCsrPoP } from '../src/ca/csr.js'
import { fingerprint } from '../src/ca/fingerprint.js'
import { generateEd25519, sealToX25519, openFromX25519, generateX25519 } from '../src/util/crypto.js'
import { randomBytes } from 'node:crypto'
function boundHost() {
const stores = createMemoryStores()
const hosts = createHostRegistry({ hosts: stores.hosts })
return { stores, hosts }
}
describe('T8 signHostLeaf — INV14 registry-gated + CSR PoP', () => {
test('active registered host + valid CSR → cert signed under CA', async () => {
const { stores, hosts } = boundHost()
const { publicKeyRaw, privateKey } = generateEd25519()
const host = await hosts.bindHost({ accountId: '11111111-1111-4111-8111-111111111111', subdomain: 'alice', agentPubkey: publicKeyRaw, enrollFpr: fingerprint(publicKeyRaw) })
const signer = createLeafSigner({ hosts: stores.hosts, signer: inProcessCaSigner(), caChainDer: [] })
const { cert } = await signer.signHostLeaf(host.hostId, publicKeyRaw, buildCsr(privateKey, publicKeyRaw))
expect(cert.length).toBeGreaterThan(0)
})
test('CSR proof-of-possession fails even for a registered active key → reject, KMS never called', async () => {
const { stores, hosts } = boundHost()
const { publicKeyRaw } = generateEd25519()
const host = await hosts.bindHost({ accountId: '11111111-1111-4111-8111-111111111111', subdomain: 'alice', agentPubkey: publicKeyRaw, enrollFpr: fingerprint(publicKeyRaw) })
const spy = inProcessCaSigner()
const signSpy = vi.spyOn(spy, 'sign')
const signer = createLeafSigner({ hosts: stores.hosts, signer: spy, caChainDer: [] })
// forge a CSR: correct embedded pub but a garbage signature
const forged = new Uint8Array(96)
forged.set(publicKeyRaw, 0)
forged.set(randomBytes(64), 32)
expect((await verifyCsrPoP(forged)).ok).toBe(false)
await expect(signer.signHostLeaf(host.hostId, publicKeyRaw, forged)).rejects.toBeInstanceOf(LeafSignError)
expect(signSpy).not.toHaveBeenCalled()
})
test('INV14 gate: unregistered pubkey → throws, KMS sign never invoked', async () => {
const { stores } = boundHost()
const { publicKeyRaw, privateKey } = generateEd25519()
const spy = inProcessCaSigner()
const signSpy = vi.spyOn(spy, 'sign')
const signer = createLeafSigner({ hosts: stores.hosts, signer: spy, caChainDer: [] })
await expect(signer.signHostLeaf('00000000-0000-4000-8000-000000000000', publicKeyRaw, buildCsr(privateKey, publicKeyRaw))).rejects.toBeInstanceOf(LeafSignError)
expect(signSpy).not.toHaveBeenCalled()
})
test('revoked host cannot be signed (INV12+INV14)', async () => {
const { stores, hosts } = boundHost()
const { publicKeyRaw, privateKey } = generateEd25519()
const host = await hosts.bindHost({ accountId: '11111111-1111-4111-8111-111111111111', subdomain: 'alice', agentPubkey: publicKeyRaw, enrollFpr: fingerprint(publicKeyRaw) })
await hosts.setHostStatus(host.hostId, 'revoked')
const signer = createLeafSigner({ hosts: stores.hosts, signer: inProcessCaSigner(), caChainDer: [] })
await expect(signer.signHostLeaf(host.hostId, publicKeyRaw, buildCsr(privateKey, publicKeyRaw))).rejects.toBeInstanceOf(LeafSignError)
})
})
describe('host content-secret wrap (FIX 3 security property)', () => {
test('only the matching private key can unwrap; raw secret absent from blob', () => {
const recipient = generateX25519()
const secret = new Uint8Array(randomBytes(32))
const blob = sealToX25519(recipient.publicKeyRaw, secret)
expect(Buffer.from(blob).includes(Buffer.from(secret))).toBe(false) // raw secret not in ciphertext
expect(Buffer.from(openFromX25519(recipient.privateKey, blob)).equals(Buffer.from(secret))).toBe(true)
const wrong = generateX25519()
expect(() => openFromX25519(wrong.privateKey, blob)).toThrow()
})
})