import { describe, expect, it } from 'vitest' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { createPublicKey, verify } from 'node:crypto' import { computeEnrollFpr, generateIdentity, generateP256Identity, identityFromPrivatePem, p256IdentityFromPrivatePem, verifySignature, } from '../src/keys/identity.js' describe('AgentIdentity (INV4)', () => { it('generates distinct keypairs', () => { const a = generateIdentity() const b = generateIdentity() expect(Buffer.from(a.publicKey).equals(Buffer.from(b.publicKey))).toBe(false) expect(a.publicKey.length).toBe(32) }) it('sign/verify round-trips', () => { const id = generateIdentity() const msg = new TextEncoder().encode('transcript-bytes') const sig = id.sign(msg) expect(verifySignature(id.publicKey, msg, sig)).toBe(true) expect(verifySignature(id.publicKey, new TextEncoder().encode('tampered'), sig)).toBe(false) }) it('enrollFpr is deterministic base64url(SHA-256(pubkey))', () => { const id = generateIdentity() expect(computeEnrollFpr(id.publicKey)).toBe(id.enrollFpr) expect(id.enrollFpr).not.toMatch(/[+/=]/) // base64url alphabet only }) it('reloads the same identity from PEM', () => { const id = generateIdentity() const pem = id.exportPrivatePkcs8Pem() const reloaded = identityFromPrivatePem(pem) expect(Buffer.from(reloaded.publicKey).equals(Buffer.from(id.publicKey))).toBe(true) expect(reloaded.enrollFpr).toBe(id.enrollFpr) }) it('security: no API returns raw private-key bytes', () => { const id = generateIdentity() // The only private-key surface is a PEM export for the 0600 keystore and an in-process // KeyObject; there is NO Uint8Array/raw-bytes getter for the private key. expect('privateKey' in id).toBe(false) const src = readFileSync( join(import.meta.dirname, '..', 'src', 'keys', 'identity.ts'), 'utf8', ) // No function exports raw private key bytes onto the network surface. expect(src).not.toMatch(/exportPrivateRaw|privateKeyBytes|toRawPrivate/) }) it('keeps the Ed25519 alg tag (no P-256 regression)', () => { expect(generateIdentity().alg).toBe('ed25519') }) }) describe('P-256 AgentIdentity (FIX H-host-2 — native frp-client key)', () => { it('generates distinct EC P-256 keypairs tagged alg=p256', () => { const a = generateP256Identity() const b = generateP256Identity() expect(a.alg).toBe('p256') expect(Buffer.from(a.publicKey).equals(Buffer.from(b.publicKey))).toBe(false) // publicKey is the EC SubjectPublicKeyInfo DER (outer SEQUENCE), importable as a public key. expect(a.publicKey[0]).toBe(0x30) const pub = createPublicKey({ key: Buffer.from(a.publicKey), format: 'der', type: 'spki' }) expect(pub.asymmetricKeyType).toBe('ec') expect(pub.asymmetricKeyDetails?.namedCurve).toBe('prime256v1') }) it('sign produces a DER ECDSA signature that verifies under SHA-256', () => { const id = generateP256Identity() const msg = new TextEncoder().encode('certificationRequestInfo-bytes') const sig = id.sign(msg) const pub = createPublicKey({ key: Buffer.from(id.publicKey), format: 'der', type: 'spki' }) expect(verify('sha256', msg, pub, sig)).toBe(true) expect(verify('sha256', new TextEncoder().encode('tampered'), pub, sig)).toBe(false) }) it('enrollFpr is deterministic base64url(SHA-256(spki))', () => { const id = generateP256Identity() expect(computeEnrollFpr(id.publicKey)).toBe(id.enrollFpr) expect(id.enrollFpr).not.toMatch(/[+/=]/) // base64url alphabet only }) it('reloads the same P-256 identity from PEM (keystore load path)', () => { const id = generateP256Identity() const pem = id.exportPrivatePkcs8Pem() const reloaded = p256IdentityFromPrivatePem(pem) expect(reloaded.alg).toBe('p256') expect(Buffer.from(reloaded.publicKey).equals(Buffer.from(id.publicKey))).toBe(true) expect(reloaded.enrollFpr).toBe(id.enrollFpr) }) it('security: P-256 identity exposes no raw private-key getter', () => { const id = generateP256Identity() expect('privateKey' in id).toBe(false) // the PEM export is the only serialization surface; it is a PRIVATE key PEM (0600 keystore only). expect(id.exportPrivatePkcs8Pem()).toContain('PRIVATE KEY') }) })