import { checkSignatureCounter } from '../../src/passkeys/signatureCounter'; /** * The rule #37 deferred to the ceremony that enforces it. * * Both halves are load-bearing and they pull in opposite directions. Requiring * an increase from every authenticator refuses the synced passkeys most people * actually use, which report zero forever by design. Requiring it from none * throws away the only signal that a hardware credential has been cloned, which * is the entire reason the column exists. */ describe('checkSignatureCounter', () => { describe('an authenticator that does not implement counters', () => { it('accepts zero against zero, and keeps accepting it', () => { // iCloud Keychain and Google Password Manager report this on every // assertion. Refusing it would refuse most real customers. expect(checkSignatureCounter(0, 0).ok).toBe(true); }); }); describe('an authenticator that does', () => { it('accepts a counter that advanced', () => { expect(checkSignatureCounter(5, 6).ok).toBe(true); expect(checkSignatureCounter(0, 1).ok).toBe(true); }); it('refuses one that stalled', () => { // Equal is not an increase. Two copies of a credential used alternately // produce exactly this. const verdict = checkSignatureCounter(7, 7); expect(verdict.ok).toBe(false); expect(verdict.reason).toMatch(/cloned/); }); it('refuses one that went backwards', () => { expect(checkSignatureCounter(9, 4).ok).toBe(false); }); // The asymmetry that stops the zero rule being an escape hatch. An // authenticator that has ever reported a real counter is held to the strict // rule from then on, so a clone cannot report zero to look like a synced // passkey and be waved through. it('refuses a drop to zero from a counter that was real', () => { const verdict = checkSignatureCounter(12, 0); expect(verdict.ok).toBe(false); expect(verdict.reason).toMatch(/did not advance/); }); }); it('reports the numbers, because a refusal is only actionable with them', () => { expect(checkSignatureCounter(12, 3).reason).toContain('stored 12'); expect(checkSignatureCounter(12, 3).reason).toContain('received 3'); }); });