import { afterEach, describe, expect, it } from 'vitest' import { mkdtempSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createPublicKey, generateKeyPairSync, verify } from 'node:crypto' import { generateIdentity, generateP256Identity } from '../src/keys/identity.js' import { KeystoreError, openKeystore } from '../src/keys/keystore.js' const dirs: string[] = [] function freshDir(): string { const d = mkdtempSync(join(tmpdir(), 'wta-ks-')) dirs.push(d) return d } afterEach(() => { while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true }) }) function mode(path: string): number { return statSync(path).mode & 0o777 } describe('Keystore (INV4/INV5)', () => { it('persists the private key 0600 and reloads it', () => { const dir = freshDir() const ks = openKeystore(dir) const id = generateIdentity() ks.saveIdentity(id) expect(mode(join(dir, 'agent.key.pem'))).toBe(0o600) const reloaded = ks.loadIdentity() expect(reloaded).not.toBeNull() expect(Buffer.from(reloaded!.publicKey).equals(Buffer.from(id.publicKey))).toBe(true) }) it('persists cert + CA chain 0600', () => { const dir = freshDir() const ks = openKeystore(dir) ks.saveCert('CERTPEM', 'CAPEM') expect(mode(join(dir, 'agent.cert.pem'))).toBe(0o600) expect(mode(join(dir, 'agent.ca.pem'))).toBe(0o600) expect(ks.loadCert()).toEqual({ certPem: 'CERTPEM', caChainPem: 'CAPEM' }) }) it('FIX 3: round-trips hostContentSecret 0600', () => { const dir = freshDir() const ks = openKeystore(dir) const secret = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) ks.saveContentSecret(secret) expect(mode(join(dir, 'content.secret'))).toBe(0o600) expect(Buffer.from(ks.loadContentSecret()!).equals(Buffer.from(secret))).toBe(true) }) it('returns null when nothing is stored yet', () => { const ks = openKeystore(freshDir()) expect(ks.loadIdentity()).toBeNull() expect(ks.loadCert()).toBeNull() expect(ks.loadContentSecret()).toBeNull() }) it('throws a typed error on a corrupt key file', () => { const dir = freshDir() writeFileSync(join(dir, 'agent.key.pem'), 'not-a-pem') expect(() => openKeystore(dir).loadIdentity()).toThrow(KeystoreError) }) it('refuses to write into a group/world-accessible dir (INV5)', () => { const dir = join(freshDir(), 'wideopen') mkdirSync(dir, { mode: 0o755 }) expect(() => openKeystore(dir).saveIdentity(generateIdentity())).toThrow(KeystoreError) }) it('keeps the Ed25519 identity byte-identical across save/load (no P-256 regression)', () => { const dir = freshDir() const ks = openKeystore(dir) const id = generateIdentity() ks.saveIdentity(id) const reloaded = ks.loadIdentity() expect(reloaded!.alg).toBe('ed25519') expect(Buffer.from(reloaded!.publicKey).equals(Buffer.from(id.publicKey))).toBe(true) expect(reloaded!.enrollFpr).toBe(id.enrollFpr) }) it('FIX H-host-2: round-trips a P-256 frp-client identity (alg + usable signing key)', () => { const dir = freshDir() const ks = openKeystore(dir) const id = generateP256Identity() ks.saveIdentity(id) expect(mode(join(dir, 'agent.key.pem'))).toBe(0o600) const reloaded = ks.loadIdentity() expect(reloaded).not.toBeNull() // loadIdentity() branched on the stored key's alg discriminant → P-256, not Ed25519. expect(reloaded!.alg).toBe('p256') // EC SubjectPublicKeyInfo DER (outer SEQUENCE) preserved exactly. expect(reloaded!.publicKey[0]).toBe(0x30) expect(Buffer.from(reloaded!.publicKey).equals(Buffer.from(id.publicKey))).toBe(true) expect(reloaded!.enrollFpr).toBe(id.enrollFpr) // The reloaded key still signs: a DER ECDSA signature that verifies under the original pubkey. const msg = new TextEncoder().encode('csr-bytes') const sig = reloaded!.sign(msg) const pub = createPublicKey({ key: Buffer.from(id.publicKey), format: 'der', type: 'spki' }) expect(verify('sha256', msg, pub, sig)).toBe(true) }) it('AG1: rejects a stored EC key on a non-P256 curve (e.g. secp384r1) with a clear error', () => { const dir = freshDir() const ks = openKeystore(dir) // Plant a valid PKCS#8 EC key on the WRONG curve directly at the key path — `ec` alone must not // be mistaken for P-256; loadIdentity must assert the named curve and fail closed. const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'secp384r1' }) const pem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString() writeFileSync(join(dir, 'agent.key.pem'), pem, { mode: 0o600 }) expect(() => ks.loadIdentity()).toThrow(KeystoreError) expect(() => ks.loadIdentity()).toThrow(/prime256v1|P-256|curve/) }) })