import { describe, test, expect } from 'vitest' import { createMemoryStores } from '../src/store/memory.js' import { createHostRegistry } from '../src/registry/hosts.js' import { createSubdomainAssigner } from '../src/subdomain/assign.js' import { createPairingIssuer } from '../src/pairing/issue.js' import { createPairingRedeemer, RedeemError } from '../src/pairing/redeem.js' import { createLeafSigner } from '../src/ca/sign.js' import { inProcessCaSigner, type CaSigner } from '../src/boot/ca-wiring.js' import { buildCsr } from '../src/ca/csr.js' import { generatePairingCode, formatPairingCode, normalizePairingCode, PAIRING_CODE_BITS } from '../src/pairing/code.js' import { sha256Hex } from '../src/util/hash.js' import { generateEd25519 } from '../src/util/crypto.js' const ACCOUNT = '11111111-1111-4111-8111-111111111111' function harness(opts?: { ttlSec?: number; maxAttempts?: number; signer?: CaSigner }) { const stores = createMemoryStores() const hosts = createHostRegistry({ hosts: stores.hosts }) const subdomains = createSubdomainAssigner({ subdomains: stores.subdomains }) const signer = opts?.signer ?? inProcessCaSigner() const leafSigner = createLeafSigner({ hosts: stores.hosts, signer, caChainDer: [new Uint8Array([1, 2, 3])] }) const issuer = createPairingIssuer({ pairing: stores.pairing, pairingTtlSec: opts?.ttlSec ?? 600 }) const redeemer = createPairingRedeemer({ pairing: stores.pairing, hosts, subdomains, leafSigner, pairingMaxRedeemAttempts: opts?.maxAttempts ?? 5, }) return { stores, issuer, redeemer } } function agentEnroll() { const { publicKeyRaw, privateKey } = generateEd25519() return { agentPubkey: publicKeyRaw, csr: buildCsr(privateKey, publicKeyRaw) } } describe('T7 pairing issuance (INV5, entropy)', () => { test('code is >= 128-bit entropy, display round-trips to canonical', () => { expect(PAIRING_CODE_BITS).toBeGreaterThanOrEqual(128) const canonical = generatePairingCode() expect(canonical.length).toBe(26) expect(normalizePairingCode(formatPairingCode(canonical))).toBe(canonical) }) test('stored row holds code_hash only — no raw code at rest (INV5)', async () => { const { stores, issuer } = harness() const { code } = await issuer.issuePairingCode(ACCOUNT) const codeHash = sha256Hex(normalizePairingCode(code)) const row = await stores.pairing.get(codeHash) expect(row).not.toBeNull() expect(JSON.stringify(row)).not.toContain(normalizePairingCode(code)) expect(row?.redeemAttempts).toBe(0) }) test('two issues → two distinct codes', async () => { const { issuer } = harness() const a = await issuer.issuePairingCode(ACCOUNT) const b = await issuer.issuePairingCode(ACCOUNT) expect(a.code).not.toBe(b.code) }) }) describe('T8 redemption + BIND + cert + content-secret', () => { test('happy path → EnrollResult with frozen fields, subject pubkey == agentPubkey', async () => { const { issuer, redeemer } = harness() const { code } = await issuer.issuePairingCode(ACCOUNT) const { agentPubkey, csr } = agentEnroll() const result = await redeemer.redeemPairingCode({ code, agentPubkey, csr }) expect(result.hostId).toMatch(/[0-9a-f-]{36}/) expect(result.subdomain.length).toBeGreaterThan(0) expect(result.cert).toContain('BEGIN CERTIFICATE') expect(result.caChain).toContain('BEGIN CERTIFICATE') expect(result.hostContentSecret.length).toBeGreaterThan(0) // subject pubkey inside the cert == agentPubkey const certJson = JSON.parse(Buffer.from(result.cert.split('\n').slice(1, -2).join(''), 'base64').toString()) const tbs = JSON.parse(Buffer.from(certJson.tbs, 'base64').toString()) expect(tbs.subjectSpki).toBe(Buffer.from(agentPubkey).toString('base64')) }) test('hostContentSecret: wrapped, raw secret absent, distinct per enrollment (FIX 3)', async () => { const { issuer, redeemer } = harness() const c1 = (await issuer.issuePairingCode(ACCOUNT)).code const c2 = (await issuer.issuePairingCode(ACCOUNT)).code const r1 = await redeemer.redeemPairingCode({ code: c1, ...agentEnroll() }) const r2 = await redeemer.redeemPairingCode({ code: c2, ...agentEnroll() }) expect(Buffer.from(r1.hostContentSecret).equals(Buffer.from(r2.hostContentSecret))).toBe(false) }) test('double-spend: two concurrent redeems → exactly one wins (CAS)', async () => { const { issuer, redeemer } = harness() const { code } = await issuer.issuePairingCode(ACCOUNT) const results = await Promise.allSettled([ redeemer.redeemPairingCode({ code, ...agentEnroll() }), redeemer.redeemPairingCode({ code, ...agentEnroll() }), ]) const ok = results.filter((r) => r.status === 'fulfilled') const rejected = results.filter((r) => r.status === 'rejected') expect(ok.length).toBe(1) expect(rejected.length).toBe(1) expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(RedeemError) expect(((rejected[0] as PromiseRejectedResult).reason as RedeemError).code).toBe('already_redeemed') }) test('expired / unknown / CSR-substitution rejected', async () => { const expiredH = harness({ ttlSec: -10 }) const { code } = await expiredH.issuer.issuePairingCode(ACCOUNT) await expect(expiredH.redeemer.redeemPairingCode({ code, ...agentEnroll() })).rejects.toMatchObject({ code: 'expired' }) const h = harness() await expect(h.redeemer.redeemPairingCode({ code: 'ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-Z', ...agentEnroll() })).rejects.toMatchObject({ code: 'unknown' }) // CSR embeds a DIFFERENT pubkey than the presented agentPubkey → bad_csr const good = (await h.issuer.issuePairingCode(ACCOUNT)).code const a = generateEd25519() const b = generateEd25519() const substitutedCsr = buildCsr(b.privateKey, b.publicKeyRaw) await expect(h.redeemer.redeemPairingCode({ code: good, agentPubkey: a.publicKeyRaw, csr: substitutedCsr })).rejects.toMatchObject({ code: 'bad_csr' }) }) test('code-scoped lockout after maxAttempts — correct code then refused (Finding-4)', async () => { const h = harness({ maxAttempts: 3 }) const { code } = await h.issuer.issuePairingCode(ACCOUNT) const bad = generateEd25519() const badCsr = buildCsr(bad.privateKey, bad.publicKeyRaw) const victim = agentEnroll() for (let i = 0; i < 3; i++) { // substitution failure against the SAME code_hash await expect(h.redeemer.redeemPairingCode({ code, agentPubkey: victim.agentPubkey, csr: badCsr })).rejects.toMatchObject({ code: 'bad_csr' }) } // now even a correct redemption is locked out await expect(h.redeemer.redeemPairingCode({ code, ...victim })).rejects.toMatchObject({ code: 'too_many_attempts' }) }) })