fn push_for_unresolved_hint(
store: &dyn ParStore,
decisions: &dyn DecisionStore,
) -> Result<(String, Duration), ParError>Expand description
What login_hint naming nobody gets instead of
push_authorization_request’s ordinary decision record: the identical
success this endpoint hands back for a real account, and nothing an
unauthenticated caller — or this store — can act on.
No account state, ever — that is the whole fix, and the whole reason
this stores nothing at all. An earlier version of this function put a
placeholder DecisionRecord through DecisionStore::put, keyed on
the raw login_hint string, so a nonexistent hint would cost the same
pending_per_account slot a real account’s push would. That reopened
the hole a different way: resolve folds every spelling of a real
account — a handle, its DID, one with a redundant :port — into the one
DID that names it, so a real account’s pending bucket is one bucket
however it is addressed, while every distinct spelling of a nonexistent
one is its own bucket forever. Push eight variously-spelled requests for
one candidate and a ninth: a real account’s ninth answers differently
from its first eight, and a made-up one’s ninth never does — the
aliasing itself is the oracle, no matter how the two buckets’ refusals
are worded. Worse, since nothing bounds how many distinct invented
strings exist, that placeholder was also a new denial-of-service surface
past the account-existence one: a handful of addresses at the per-address
rate limit could keep pending_total permanently full with entries no
real account ever asked for, since before this a stranger could not
occupy a slot without naming one.
The fix an account-keyed bound cannot have is not to bound unresolved
hints at all — there is no account here for a bound to be of. A fresh
request_uri is minted through ParStore::mint and handed back with
the same expires_in a real push gets, and nothing is stored under it
anywhere: not in ParStore, so GET /oauth/authorize answers
crate::oauth::authorize::AuthorizeError::UnknownRequestUri for it,
as it does for a real one this server has swept; not in
DecisionStore, so
bot.did.getAuthorization answers not-found for the same reason it
always would — no credential can authenticate as a string that resolved
to no account.
That leaves one thing this must still answer identically to a real
push: DecisionStore::check_room, the server’s own bound, which is
a fact about this deployment’s load and not about login_hint at all —
true for every caller alike, so answering it truthfully leaks nothing.
Skipping this check would make a full store itself an oracle (real
accounts start answering 403, unresolved hints never would), so the
close reads it — without writing anything, whichever way it answers.