Skip to content

fix(federation): share the registration cache fairly between superiors - #620

Merged
osanderson merged 1 commit into
mainfrom
fix/federation-cache-quota
Oct 9, 2026
Merged

osanderson merged 1 commit into
mainfrom
fix/federation-cache-quota

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

The automatic-registration cache now limits how much of it any one federation member can occupy:

  • per immediate superior: at most 512 entries;
  • per branch of a Trust Anchor: at most 2048;
  • overall: 4096, unchanged.

An intermediate that mints many Relying Parties can now only ever displace its own entries.

Merge after the v0.52.0 release PR (#616). This is a non-breaking fix intended for v0.52.1.

Why

From the v0.52.0 pre-release review. Since #610 the cache was capped at 4096, and once full it cached nothing new rather than evict a still-valid entry. That stopped an attacker pushing busy clients out. But an intermediate that minted and refreshed 4096 Relying Parties once per cache lifetime could keep the cache full indefinitely, so every other federation client paid a full Trust Chain resolution on every request.

Design

Each cached registration records where it sits in its resolved Trust Chain (ResolvedEntity.Chain):

  • its immediate superior: Chain[1], the entity that issued its Subordinate Statement and the only one able to mint Relying Parties beside it;
  • its branch: Chain[len-2], the Trust Anchor's own subordinate the chain runs through. This also covers the intermediates an attacker-controlled intermediate mints beneath itself, each of which would otherwise be a new "superior" with its own quota.

Policy on insert:

Situation Outcome
Superior at its quota (512) Evict that superior's own oldest entry, then cache.
Branch at its cap (2048) Drop expired entries; if still at the cap, don't cache.
Whole cache full (4096) Drop expired entries; if still full, don't cache.
Directly under a Trust Anchor (no branch) Counted only against the overall 4096. Such a Relying Party can't mint siblings, and the Trust Anchor is trusted.

A still-valid entry is never evicted for another superior's registration.

Cost:

  • lookups stay O(1), and so does an eviction (a per-superior list in insertion order);
  • the expired-entry sweep runs only when a limit is reached, as before;
  • an entry found expired on lookup is still removed, releasing its counts.

The cache moves into its own unexported type (federation/registration_cache.go). AutomaticRegistrationConfig.MaxCacheAge's doc and the federation guide describe the policy.

Verification

  • New tests:
    • the attack: one intermediate minting 3× its quota keeps exactly its newest 512, and Relying Parties under other superiors stay and get cached;
    • nested sub-intermediates filling a branch;
    • a full cache never evicting a valid entry;
    • Trust-Anchor leaves having no per-superior quota;
    • refresh moving an entry to the back without double-counting;
    • expiry on lookup releasing counts;
    • chainPosition;
    • an end-to-end check that a real resolution records the chain position (via a test-only accessor in export_test.go).
  • Existing cache tests pass, ported to the new type.
  • Mutation checks: 8 mutants, all caught:
    • no own-superior eviction;
    • no branch cap;
    • evicting instead of skipping when full;
    • expired entries kept on lookup;
    • branch counts not released;
    • a quota applied to Trust-Anchor leaves;
    • register dropping the chain position;
    • the wrong branch computed.
  • Wider checks, all clean:
    • go vet, and go test on Go 1.27 and 1.26.9;
    • go test -race ./federation/...;
    • golangci-lint (0 issues), and the GOOS=linux GOARCH=arm vet;
    • every example module's vet and -race tests, including federated-union.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The automatic-registration cache held at most 4096 registrations and,
once full, cached nothing new rather than evict a valid entry. That kept
an attacker from pushing out busy clients, but an intermediate that
minted and refreshed 4096 Relying Parties once per cache lifetime kept it
full indefinitely, so every other federation client paid a full Trust
Chain resolution on every request.

Each cached registration is now counted against its immediate superior
(the entity that issued its Subordinate Statement, the only one able to
mint Relying Parties beside it) and against its branch (the Trust
Anchor's own subordinate its chain runs through, which also covers the
intermediates an intermediate mints beneath itself):

- a superior holds at most 512 entries; its newest registration evicts
  its own oldest, never anyone else's;
- a branch holds at most 2048; past that, and past the overall 4096,
  expired entries are dropped first and otherwise the new registration
  isn't cached;
- Relying Parties directly under a Trust Anchor can't mint siblings, so
  they count only against the overall limit.

An attacker able to mint Relying Parties can now only ever displace its
own. Lookups stay O(1) and an eviction is O(1); the expired-entry sweep
runs only when a limit is reached. The cache moves into its own type,
and MaxCacheAge's doc and the federation guide describe the policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@osanderson
osanderson force-pushed the fix/federation-cache-quota branch from ce66e1e to e239dd6 Compare October 9, 2026 12:37
@sonarqubecloud

sonarqubecloud Bot commented Oct 9, 2026

Copy link
Copy Markdown

@osanderson
osanderson merged commit bcdace5 into main Oct 9, 2026
18 checks passed
@osanderson
osanderson deleted the fix/federation-cache-quota branch October 9, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant