import { describe, expect, it } from 'vitest' import { computeEnrollFpr, resolvePin, verifyPinnedFingerprint } from '../src/fingerprint.js' import { MemoryDevicePinStore } from '../src/keystore.js' import { hexToBytes } from './helpers.js' import fprVector from './vectors/fingerprint.json' with { type: 'json' } describe('T4 fingerprint / TOFU pin', () => { const pubkey = hexToBytes(fprVector.agentPubkey) it('computeEnrollFpr matches the frozen §4.2 enroll_fpr vector (agent↔browser parity)', () => { expect(computeEnrollFpr(pubkey)).toBe(fprVector.enrollFpr) expect(computeEnrollFpr(pubkey).startsWith('sha256:')).toBe(true) }) it('verifyPinnedFingerprint: true on match, false on any-byte / length diff', () => { expect(verifyPinnedFingerprint(pubkey, fprVector.enrollFpr)).toBe(true) expect(verifyPinnedFingerprint(pubkey, fprVector.enrollFpr + 'x')).toBe(false) expect(verifyPinnedFingerprint(pubkey, 'sha256:zzzz')).toBe(false) }) it('resolvePin: empty store → tofu-first-use with the recomputed fpr', async () => { const store = new MemoryDevicePinStore() const r = await resolvePin(store, 'h1', pubkey) expect(r.outcome).toBe('tofu-first-use') expect(r.computedFpr).toBe(fprVector.enrollFpr) expect(r.pinnedFpr).toBeNull() }) it('resolvePin: matching pin → match', async () => { const store = new MemoryDevicePinStore() await store.pin('h1', fprVector.enrollFpr) const r = await resolvePin(store, 'h1', pubkey) expect(r.outcome).toBe('match') }) it('resolvePin: differing pin → mismatch (MITM/rotation signal), never auto-repins', async () => { const store = new MemoryDevicePinStore() await store.pin('h1', 'sha256:some-other-host-fpr') const r = await resolvePin(store, 'h1', pubkey) expect(r.outcome).toBe('mismatch') // The stored pin is untouched (no TOFU downgrade). expect(await store.get('h1')).toBe('sha256:some-other-host-fpr') }) it('security: decision keys only off the LOCALLY-recomputed fpr, not a caller string', async () => { // A different pubkey hashes to a different fpr even if a (hypothetically relay-supplied) pinned // string happened to equal the honest host's fpr — resolvePin recomputes from bytes. const store = new MemoryDevicePinStore() await store.pin('h1', fprVector.enrollFpr) const otherPubkey = pubkey.slice() otherPubkey[0]! ^= 0xff const r = await resolvePin(store, 'h1', otherPubkey) expect(r.outcome).toBe('mismatch') expect(r.computedFpr).not.toBe(fprVector.enrollFpr) }) })