diff --git a/src/core/directory-admission.ts b/src/core/directory-admission.ts index 4c9e5504..df856526 100644 --- a/src/core/directory-admission.ts +++ b/src/core/directory-admission.ts @@ -41,7 +41,6 @@ export interface DirectoryAdmissionDeps { } interface Verdict { - proof: string; ok: boolean; /** When a refusal stops being remembered (epoch ms). */ until: number; @@ -65,7 +64,7 @@ export function directoryAdmission( deps: Readonly, ): (entries: readonly DirectoryEntry[]) => Promise { const { gatewayTrust, verifyMembership } = deps; - /** Keyed by kind and device, since a device can carry one proof of each kind. */ + /** Keyed by kind, device and proof. A directory event can carry several proofs for one device at once, a stale copy beside a fresh one, so a slot per device would be overwritten by each in turn and every event would verify them all again. */ const verdicts = new Map(); const remember = (key: string, verdict: Readonly): void => { @@ -123,9 +122,9 @@ export function directoryAdmission( const proof = readMembershipProof(entry.advert, field); if (proof === undefined) continue; - const key = `${kind}:${deviceHex}`; + const key = `${kind}:${deviceHex}:${proof}`; const known = verdicts.get(key); - if (known?.proof === proof) { + if (known !== undefined) { if (known.ok && gatewayTrust.isVouchedBy(deviceHex, kind)) { vouched = true; continue; @@ -137,7 +136,6 @@ export function directoryAdmission( const vouch = await vouchedFor(proof, deviceHex, kind); remember(key, { - proof, ok: vouch !== undefined, until: Date.now() + NEGATIVE_VERDICT_TTL_MS, }); diff --git a/src/test/directory-admission.unit.test.ts b/src/test/directory-admission.unit.test.ts index fd06dc4c..27aeaeac 100644 --- a/src/test/directory-admission.unit.test.ts +++ b/src/test/directory-admission.unit.test.ts @@ -146,6 +146,28 @@ describe("directoryAdmission", () => { expect(verifyMembership).toHaveBeenCalledTimes(1); }); + it("does not re-verify either of two proofs for one device that arrive in alternation", async () => { + const trust = new GatewayTrust(); + trust.addPrincipal(PRINCIPAL); + const verifyMembership = verifier(false); + const admit = directoryAdmission({ gatewayTrust: trust, verifyMembership }); + + await admit([ + entry("alternating", "proof-a"), + entry("alternating", "proof-b"), + ]); + await admit([ + entry("alternating", "proof-a"), + entry("alternating", "proof-b"), + ]); + await admit([ + entry("alternating", "proof-b"), + entry("alternating", "proof-a"), + ]); + + expect(verifyMembership).toHaveBeenCalledTimes(2); + }); + it("verifies a device's new proof even while its old one is still good, so its trust is renewed before the old one lapses", async () => { const trust = new GatewayTrust(); trust.addPrincipal(PRINCIPAL);