/** * B1 acceptance (FIX H-host-2, H-host-4) — the P-256 HOST frp-client leaf signer. Proves an issued * leaf is a REAL, tool-parseable, signature-verifiable X.509 v3 cert off the P-256 frp-client-CA, * carrying the dNSName ENFORCEMENT key + the host SPIFFE-ID (which relay-auth's parser accepts), * EKU clientAuth + CA:false; that the registry gate rejects unregistered / revoked / pubkey-mismatch * UNIFORMLY without ever invoking the CA signer; and that a P-256 CSR built by the AGENT's own * `enroll/csr.ts` passes `verifyCsrPoPEc` and flows its embedded pubkey through to issuance. */ import 'reflect-metadata' import { describe, test, expect, vi } from 'vitest' import * as x509 from '@peculiar/x509' import { AsnConvert } from '@peculiar/asn1-schema' import { Certificate } from '@peculiar/asn1-x509' import { webcrypto, generateKeyPairSync, randomUUID } from 'node:crypto' import { parseSpiffeId, spiffeIdFor } from 'relay-auth/src/agent/spiffe.js' import { createMemoryStores } from '../src/store/memory.js' import { createHostRegistry } from '../src/registry/hosts.js' import { fingerprint } from '../src/ca/fingerprint.js' import { assembleCertificate } from '../src/ca/x509-assembler.js' import { buildCsrEc, verifyCsrPoPEc } from '../src/ca/csr-ec.js' import { inProcessP256CaSigner, type CaSigner } from '../src/boot/ca-wiring.js' import { createFrpClientLeafSigner } from '../src/ca/frpclient-issue.js' import { LeafSignError } from '../src/ca/sign.js' // Cross-track proof (vi): drive the ACTUAL agent-side P-256 identity + CSR encoder. import { generateP256Identity } from '../../agent/src/keys/identity.js' import { buildCsr as buildAgentCsr } from '../../agent/src/enroll/csr.js' x509.cryptoProvider.set(webcrypto) const DAY_MS = 24 * 60 * 60 * 1000 const TRUST_DOMAIN = 'terminal.yaojia.wang' const DNS_ZONE = 'terminal.yaojia.wang' interface FrpClientCa { readonly caSigner: CaSigner readonly caCert: x509.X509Certificate readonly caDer: Uint8Array } /** Self-signed P-256 `frp-client-CA` (subject key == signer key) so leaves chain to a real CA. */ async function makeFrpClientCa(): Promise { const caKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' }) const caSpki = new Uint8Array(caKeys.publicKey.export({ format: 'der', type: 'spki' })) const caSigner = inProcessP256CaSigner(caKeys.privateKey) const now = Date.now() const caDer = await assembleCertificate({ subjectPublicKey: caSpki, subject: 'CN=frp-client-CA', issuer: 'CN=frp-client-CA', serialNumber: Uint8Array.from([0x01]), notBefore: new Date(now - DAY_MS), notAfter: new Date(now + 365 * DAY_MS), extensions: [ new x509.BasicConstraintsExtension(true, undefined, true), new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), ], signer: caSigner, sigAlg: 'ecdsa-p256', }) return { caSigner, caCert: new x509.X509Certificate(caDer), caDer } } async function importP256Public(spkiDer: Uint8Array): Promise { return webcrypto.subtle.importKey('spki', new Uint8Array(spkiDer), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']) } async function spkiOf(pub: CryptoKey): Promise { return new Uint8Array(await webcrypto.subtle.exportKey('spki', pub)) } /** A memory-store host registry with a P-256 host bound under `subdomain`, plus its CSR + SPKI. */ async function boundHost(subdomain = 'alice') { const stores = createMemoryStores() const hosts = createHostRegistry({ hosts: stores.hosts }) const { der: csr, keys } = await buildCsrEc(`CN=${subdomain}`) const spki = await spkiOf(keys.publicKey) const accountId = randomUUID() const host = await hosts.bindHost({ accountId, subdomain, agentPubkey: spki, enrollFpr: fingerprint(spki) }) return { stores, hosts, host, csr, spki, accountId } } function makeSigner(ca: FrpClientCa, stores: ReturnType) { return createFrpClientLeafSigner({ hosts: stores.hosts, signer: ca.caSigner, issuerName: ca.caCert.subjectName, caChainDer: [ca.caDer], trustDomain: TRUST_DOMAIN, dnsZone: DNS_ZONE, }) } /** Assert a promise rejects with a `LeafSignError` and return it so the caller can check `.code`. */ async function expectLeafSignError(p: Promise): Promise { const err = await p.then( () => { throw new Error('expected the promise to reject with LeafSignError') }, (e: unknown) => e, ) expect(err).toBeInstanceOf(LeafSignError) return err as LeafSignError } describe('frpclient-issue — (i) issued leaf re-parses as X.509 & (ii) chains to the frp-client-CA', () => { test('leaf re-parses via new x509.X509Certificate and its signature verifies against the CA pubkey', async () => { const ca = await makeFrpClientCa() const { stores, host, csr, spki } = await boundHost() const { cert, caChain } = await makeSigner(ca, stores).signHostLeaf(host.hostId, spki, csr) expect(() => new x509.X509Certificate(cert)).not.toThrow() const leaf = new x509.X509Certificate(cert) expect(leaf.issuer).toBe(ca.caCert.subject) // issuer DN == CA subject DN (chain sanity) const caPub = await importP256Public(ca.caSigner.publicKeyRaw) expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true) // negative: does NOT verify under a different CA key const otherCa = await importP256Public(inProcessP256CaSigner().publicKeyRaw) expect(await leaf.verify({ publicKey: otherCa, signatureOnly: true })).toBe(false) expect(caChain).toEqual([ca.caDer]) }) test('subject key is the enrolled host P-256 key (SPKI byte-equal)', async () => { const ca = await makeFrpClientCa() const { stores, host, csr, spki } = await boundHost() const { cert } = await makeSigner(ca, stores).signHostLeaf(host.hostId, spki, csr) const leaf = new x509.X509Certificate(cert) expect(Buffer.from(leaf.publicKey.rawData).equals(Buffer.from(spki))).toBe(true) }) }) describe('frpclient-issue — (iii) SAN carries dNSName + host SPIFFE URI', () => { test("SAN = { dNSName '.terminal.yaojia.wang', URI host-SPIFFE } and parseSpiffeId accepts it as kind host", async () => { const ca = await makeFrpClientCa() const { stores, host, csr, spki, accountId } = await boundHost('alice') const { cert } = await makeSigner(ca, stores).signHostLeaf(host.hostId, spki, csr) const leaf = new x509.X509Certificate(cert) const san = leaf.getExtension(x509.SubjectAlternativeNameExtension) const names = san!.names.toJSON() expect(names).toContainEqual({ type: 'dns', value: 'alice.terminal.yaojia.wang' }) const uri = names.find((n) => n.type === 'url')!.value expect(uri).toBe(spiffeIdFor(accountId, 'alice', TRUST_DOMAIN, 'host')) // relay-auth's OWN parser accepts the URI as a host identity (never drifts from the verifier). expect(parseSpiffeId(uri)).toEqual({ accountId, id: 'alice', kind: 'host' }) }) }) describe('frpclient-issue — (iv) EKU clientAuth + CA:false + KeyUsage digitalSignature', () => { test('leaf is a client-auth end-entity cert', async () => { const ca = await makeFrpClientCa() const { stores, host, csr, spki } = await boundHost() const { cert } = await makeSigner(ca, stores).signHostLeaf(host.hostId, spki, csr) const leaf = new x509.X509Certificate(cert) const bc = leaf.getExtension(x509.BasicConstraintsExtension) expect(bc!.ca).toBe(false) const eku = leaf.getExtension(x509.ExtendedKeyUsageExtension) expect(eku!.usages).toContain(x509.ExtendedKeyUsage.clientAuth) const ku = leaf.getExtension(x509.KeyUsagesExtension) expect(ku!.usages & x509.KeyUsageFlags.digitalSignature).not.toBe(0) // signatureAlgorithm is ecdsa-with-SHA256 (a P-256 leaf, not Ed25519). const parsed = AsnConvert.parse(cert.buffer.slice(cert.byteOffset, cert.byteOffset + cert.byteLength) as ArrayBuffer, Certificate) expect(parsed.signatureAlgorithm.algorithm).toBe('1.2.840.10045.4.3.2') }) }) describe('frpclient-issue — (v) registry gate rejects uniformly, CA signer never invoked', () => { test('unregistered host → LeafSignError(not_registered), sign() never called', async () => { const ca = await makeFrpClientCa() const signSpy = vi.spyOn(ca.caSigner, 'sign') const { stores, csr, spki } = await boundHost() const err = await expectLeafSignError(makeSigner(ca, stores).signHostLeaf(randomUUID(), spki, csr)) expect(err.code).toBe('not_registered') expect(signSpy).not.toHaveBeenCalled() }) test('revoked host → LeafSignError(not_registered), sign() never called', async () => { const ca = await makeFrpClientCa() const signSpy = vi.spyOn(ca.caSigner, 'sign') const { stores, hosts, host, csr, spki } = await boundHost() await hosts.setHostStatus(host.hostId, 'revoked') const err = await expectLeafSignError(makeSigner(ca, stores).signHostLeaf(host.hostId, spki, csr)) expect(err.code).toBe('not_registered') expect(signSpy).not.toHaveBeenCalled() }) test('registry pubkey mismatch (present a different key than registered) → not_registered', async () => { const ca = await makeFrpClientCa() const signSpy = vi.spyOn(ca.caSigner, 'sign') const { stores, host } = await boundHost() // host registered under key A // A DIFFERENT P-256 key B — CSR PoP + substitution checks pass (embedded == presented), but the // registry stored key A, so step 3 rejects. const { der: csrB, keys: keysB } = await buildCsrEc('CN=alice') const spkiB = await spkiOf(keysB.publicKey) const err = await expectLeafSignError(makeSigner(ca, stores).signHostLeaf(host.hostId, spkiB, csrB)) expect(err.code).toBe('not_registered') expect(signSpy).not.toHaveBeenCalled() }) test('bad CSR proof-of-possession → LeafSignError(csr_rejected), sign() never called', async () => { const ca = await makeFrpClientCa() const signSpy = vi.spyOn(ca.caSigner, 'sign') const { stores, host, csr, spki } = await boundHost() const forged = Uint8Array.from(csr) forged[forged.length - 1] = forged[forged.length - 1]! ^ 0xff // corrupt the signature const err = await expectLeafSignError(makeSigner(ca, stores).signHostLeaf(host.hostId, spki, forged)) expect(err.code).toBe('csr_rejected') expect(signSpy).not.toHaveBeenCalled() }) test('all rejects share the SAME uniform message (never leaks which check failed)', async () => { const ca = await makeFrpClientCa() const { stores, hosts, host, csr, spki } = await boundHost() const signer = makeSigner(ca, stores) const unregistered = await expectLeafSignError(signer.signHostLeaf(randomUUID(), spki, csr)) await hosts.setHostStatus(host.hostId, 'revoked') const revoked = await expectLeafSignError(signer.signHostLeaf(host.hostId, spki, csr)) expect(unregistered.message).toBe(revoked.message) expect(unregistered.message).toBe('leaf signing refused') }) }) describe('frpclient-issue — (vi) an AGENT-built P-256 CSR passes verifyCsrPoPEc & issues', () => { test('agent enroll/csr.ts CSR: verifyCsrPoPEc ok + embedded pubkey flows into the leaf', async () => { // Drive the REAL agent-side encoder (not the control-plane test helper). const id = generateP256Identity() const agentCsrPem = buildAgentCsr(id, 'alice.terminal.yaojia.wang') const agentCsrDer = new Uint8Array( Buffer.from(agentCsrPem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, ''), 'base64'), ) // (a) the control-plane P-256 PoP verifier accepts the agent's hand-rolled CSR. const pop = await verifyCsrPoPEc(agentCsrDer) expect(pop.ok).toBe(true) // its embedded pubkey is exactly the agent identity's EC SPKI. expect(Buffer.from(pop.embeddedPubSpki).equals(Buffer.from(id.publicKey))).toBe(true) // (b) that same pubkey flows through issuance: register it, sign, and the leaf carries it. const ca = await makeFrpClientCa() const stores = createMemoryStores() const hosts = createHostRegistry({ hosts: stores.hosts }) const accountId = randomUUID() const host = await hosts.bindHost({ accountId, subdomain: 'alice', agentPubkey: id.publicKey, enrollFpr: fingerprint(id.publicKey), }) const { cert } = await makeSigner(ca, stores).signHostLeaf(host.hostId, id.publicKey, agentCsrDer) const leaf = new x509.X509Certificate(cert) expect(Buffer.from(leaf.publicKey.rawData).equals(Buffer.from(id.publicKey))).toBe(true) const caPub = await importP256Public(ca.caSigner.publicKeyRaw) expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true) }) })