Identities for entities did.bot

serve: one account can fill the DPoP replay window and lock every other account out #213

Closed permadeath.com
Closed opened by permadeath.com
  • crates/didbot-serve/src/replay.rs:22: one REPLAY_CAPACITY of 100,000 entries is shared by every account. A full window refuses the next proof with ReplayWindowFull (crates/didbot-serve/src/oauth/dpop.rs:704-712) rather than evicting.
  • crates/didbot-serve/src/auth.rs:613: an entry is spent once the access token checks out, so the caller only needs one valid DPoP-bound grant. crates/didbot-serve/src/routes/session.rs:27 (getSession) is one DPoP route that charges no per-account rate, so nothing slows the caller.
  • Failure: one account loops on getSession with a fresh jti each time. At DPOP_SKEW = 5 minutes (oauth/mod.rs:141) each entry lives 5 to 10 minutes, so about 170 to 330 requests a second fills the window. Every other account's DPoP-bound call, and every refresh at /oauth/token, then fails with ReplayWindowFull until entries expire. Open PR https://tangled.org/permadeath.com/didbot/pulls/1211 shortens the hold to the nonce lifetime (two minutes at most), which raises the rate needed to about 830 a second but leaves the shared window.

Suggested fix: count live entries by the account the credential names (the client at /oauth/token). Refuse an account with ReplayWindowFull alone once it holds more than REPLAY_CAPACITY / N entries. Keep the global cap as a backstop. Only authenticated proofs reach accept, so every key is a real grant, and an unrated DPoP route then fills only the caller's own share.

Needs the owner's decision on N, which is a new parameter: it needs its own config key and documented range.

Source: https://tangled.org/permadeath.com/didbot/pulls/1211 ("Not built here, a per-account share of the replay window")

Filed by Claude (agent) from a static read of main at 0adf906a

Fixed in #1240. Each replay-window entry is charged to the account the credential names (the code or grant account at /oauth/token, so one app serving many accounts is not one share). Past [oauth] dpop_proofs_per_account (default 2,000, range 100 to 100,000) that account alone gets ReplayShareFull; the window-wide cap stays as a backstop.

Sign up or Sign in to add to the discussion.