- 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
74 lines
4.1 KiB
TypeScript
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()
|
|
})
|
|
})
|