/** * 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 } { 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)) } }) })