Files
web-terminal/control-plane/test/ca-wiring.test.ts
Yaojia Wang af630143de feat(control-plane): production native-CA wiring for on-disk P-256 CAs (A2-prep)
Wire the prod boot to issue frp-client + device leaves from the existing on-disk
P-256 CAs: NATIVE_* env vars, a file-backed KmsResolver (loads the PEM key,
validates prime256v1, never logs key material), buildFileBackedNativeCas threaded
into buildControlPlane via a nativeCas override. Fail-closed on partial NATIVE_*.
281 tests pass.
2026-07-18 15:04:04 +02:00

100 lines
4.5 KiB
TypeScript

/**
* A2-prep — file-backed KMS resolver unit tests. Proves `fileKmsResolver` loads an on-disk PKCS#8
* PEM P-256 CA key and exposes it as a `CaSigner` whose signature verifies against the key's public
* half (the single-owner file-custody reality of the VPS deploy), and fails loud on bad material
* (missing file, malformed PEM, wrong curve) — NEVER leaking key bytes (INV9).
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest'
import { generateKeyPairSync, createPublicKey, verify as nodeVerify, randomBytes } from 'node:crypto'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileKmsResolver, fileKeyRef } from '../src/boot/ca-wiring.js'
let dir: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'cp-cawiring-'))
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
})
/** Write a fresh P-256 PKCS#8 PEM key to disk; return its path + the matching public KeyObject. */
function writeP256Key(name: string): { path: string; publicKey: ReturnType<typeof createPublicKey> } {
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
const pem = privateKey.export({ format: 'pem', type: 'pkcs8' }) as string
const path = join(dir, name)
writeFileSync(path, pem)
return { path, publicKey }
}
describe('fileKmsResolver — P-256 on-disk key custody', () => {
test('resolves a file: ref, loads the P-256 key, and signs verifiably', async () => {
const { path, publicKey } = writeP256Key('frp.key.pem')
const resolver = fileKmsResolver()
const { signer, policyRestrictedToServicePrincipal } = await resolver.resolve(fileKeyRef(path))
expect(policyRestrictedToServicePrincipal).toBe(true)
const tbs = randomBytes(48)
const sig = await signer.sign(new Uint8Array(tbs))
// signer emits raw P1363 r||s (64 bytes) — the same shape hardware/`inProcessP256CaSigner` produce.
expect(sig.length).toBe(64)
const ok = nodeVerify('sha256', tbs, { key: publicKey, dsaEncoding: 'ieee-p1363' }, Buffer.from(sig))
expect(ok).toBe(true)
// the signer's public key equals the on-disk key's SPKI DER.
const spki = new Uint8Array(publicKey.export({ format: 'der', type: 'spki' }) as Buffer)
expect(Buffer.from(signer.publicKeyRaw).equals(Buffer.from(spki))).toBe(true)
})
test('resolves a bare path (no file: prefix)', async () => {
const { path, publicKey } = writeP256Key('bare.key.pem')
const { signer } = await fileKmsResolver().resolve(path)
const tbs = randomBytes(32)
const sig = await signer.sign(new Uint8Array(tbs))
expect(nodeVerify('sha256', tbs, { key: publicKey, dsaEncoding: 'ieee-p1363' }, Buffer.from(sig))).toBe(true)
})
test('missing file → rejects, never echoing the path contents', async () => {
await expect(fileKmsResolver().resolve(fileKeyRef(join(dir, 'nope.pem')))).rejects.toThrow(/unreadable/)
})
test('malformed PEM → rejects', async () => {
const path = join(dir, 'bad.pem')
writeFileSync(path, 'not a pem key')
await expect(fileKmsResolver().resolve(fileKeyRef(path))).rejects.toThrow(/not a valid PEM private key/)
})
test('non-P-256 key (Ed25519) → rejected (curve mismatch, fail-closed)', async () => {
const { privateKey } = generateKeyPairSync('ed25519')
const path = join(dir, 'ed.key.pem')
writeFileSync(path, privateKey.export({ format: 'pem', type: 'pkcs8' }) as string)
await expect(fileKmsResolver().resolve(fileKeyRef(path))).rejects.toThrow(/P-256/)
})
test('wrong-curve EC key (P-384) → rejected', async () => {
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-384' })
const path = join(dir, 'p384.key.pem')
writeFileSync(path, privateKey.export({ format: 'pem', type: 'pkcs8' }) as string)
await expect(fileKmsResolver().resolve(fileKeyRef(path))).rejects.toThrow(/P-256/)
})
test('empty file: ref path → rejected', async () => {
await expect(fileKmsResolver().resolve('file:')).rejects.toThrow(/empty path/)
})
test('never leaks key material in the error message (INV9)', async () => {
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-384' })
const pem = privateKey.export({ format: 'pem', type: 'pkcs8' }) as string
const path = join(dir, 'secret.key.pem')
writeFileSync(path, pem)
try {
await fileKmsResolver().resolve(fileKeyRef(path))
throw new Error('expected rejection')
} catch (e) {
expect(e instanceof Error ? e.message : '').not.toContain(pem.slice(40, 80))
}
})
})