feat(identity)!: resolver abstraction, identity caching and SSRF-safe fetching #120

Closed
opened 2026-09-24 12:39:04 +00:00 by Grandiras · 0 comments
Owner

Part of the v0.7.0 update cycle (wave 4). DID and handle resolution is uncached, has no abstraction, is written three times, and is not SSRF-hardened. The worst consequence: FirehoseVerifier resolves the DID document over HTTP for every commit, so a signature-verifying firehose consumer handles about 10–30 commits/s against a 1–2k/s firehose, and it hammers plc.directory.

Abstraction

  • IDidResolver, IHandleResolver and IIdentityResolver (bidirectional handle verification, with handle.invalid on failure), returning ResolvedIdentity(Did, Handle?, HandleVerified, Uri? PdsEndpoint, DidDocument).
  • One immutable DidDocument. Delete the OAuth copy (Auth/OAuth/OAuthModels.cs) and the duplicate DidService types. Provide GetServiceEndpoint(fragment, type?), GetVerificationKey(fragment) and a validated GetHandle().
  • Everything consumes the interfaces: OAuth discovery (which loses its private DID resolver, handle validation and hard-coded plc.directory), FirehoseVerifier, SpaceCredentialProvider, SpaceSyncer, the Spaces server resolvers and SpaceAuthority.
  • DidResolutionException(kind) replaces PlcException/DidWebException. Today PlcException escapes the Spaces error contracts.

Caching

  • A caching decorator: bounded LRU with stale and max TTL, negative caching, single-flight per DID, Invalidate(did), and an optional IDistributedCache backing. It replaces CachingSpaceDidDocumentResolver, which is unbounded, has no negative cache and no single-flight.
  • Refetch once on signature-verification failure, as the sync spec requires. TypedFirehoseConsumer invalidates on #identity events.
  • Use the parsed-key cache from #112 on every verification path.
  • SpaceServiceAuthVerifier.IsRepoHostAsync resolves its two DIDs concurrently.

Hardened fetching (SSRF)

  • One policy for every identity fetch (did:web, PLC, .well-known/atproto-did):

    • https only;
    • a SocketsHttpHandler.ConnectCallback rejecting loopback/private/link-local/CGNAT addresses after DNS;
    • the did:web port rule and no path-based did:web;
    • response-size caps (for example 64 KiB for DID documents) and short timeouts;
    • an explicit development opt-out for local or private PLC mirrors.

    Today the two DID resolvers disagree: the one the space server uses accepts any host:port, for example did:web:internal.corp%3A6379.

  • Handle resolution: DNS TXT (a configurable DoH endpoint; today it is hard-coded to dns.google) plus HTTPS well-known, keeping the current fail-closed-on-conflict semantics.

Lexicon and PLC

  • Add com.atproto.identity.resolveIdentity, resolveDid and refreshIdentity (+ defs#identityInfo).
  • PlcClient takes an HttpClient plus an explicit directory URL (it relies on BaseAddress today). Keep the audit, log, data and health APIs and add tests for them. Add PLC /export and the /export/stream WebSocket.
  • Remove the duplicated PLC models (PlcOperationService = DidService, PlcData ⊂ PlcOperation, …).

Acceptance

  • A table-driven SSRF policy test.
  • Cache tests (TTL, negative, single-flight, invalidate).
  • A firehose verification test showing zero network calls on cache hits.
  • CHANGELOG Breaking changes (moved and renamed types, the exception type), Security (SSRF), Changed (caching).
Part of the v0.7.0 update cycle (wave 4). DID and handle resolution is uncached, has no abstraction, is written three times, and is not SSRF-hardened. The worst consequence: `FirehoseVerifier` resolves the DID document **over HTTP for every commit**, so a signature-verifying firehose consumer handles about 10–30 commits/s against a 1–2k/s firehose, and it hammers plc.directory. ## Abstraction - [ ] `IDidResolver`, `IHandleResolver` and `IIdentityResolver` (bidirectional handle verification, with `handle.invalid` on failure), returning `ResolvedIdentity(Did, Handle?, HandleVerified, Uri? PdsEndpoint, DidDocument)`. - [ ] **One** immutable `DidDocument`. Delete the OAuth copy (`Auth/OAuth/OAuthModels.cs`) and the duplicate `DidService` types. Provide `GetServiceEndpoint(fragment, type?)`, `GetVerificationKey(fragment)` and a validated `GetHandle()`. - [ ] Everything consumes the interfaces: OAuth discovery (which loses its private DID resolver, handle validation and hard-coded `plc.directory`), `FirehoseVerifier`, `SpaceCredentialProvider`, `SpaceSyncer`, the Spaces server resolvers and `SpaceAuthority`. - [ ] `DidResolutionException(kind)` replaces `PlcException`/`DidWebException`. Today `PlcException` escapes the Spaces error contracts. ## Caching - [ ] A caching decorator: bounded LRU with stale and max TTL, negative caching, single-flight per DID, `Invalidate(did)`, and an optional `IDistributedCache` backing. It replaces `CachingSpaceDidDocumentResolver`, which is unbounded, has no negative cache and no single-flight. - [ ] Refetch once on signature-verification failure, as the sync spec requires. `TypedFirehoseConsumer` invalidates on `#identity` events. - [ ] Use the parsed-key cache from #112 on every verification path. - [ ] `SpaceServiceAuthVerifier.IsRepoHostAsync` resolves its two DIDs concurrently. ## Hardened fetching (SSRF) - [ ] One policy for every identity fetch (did:web, PLC, `.well-known/atproto-did`): - `https` only; - a `SocketsHttpHandler.ConnectCallback` rejecting loopback/private/link-local/CGNAT addresses **after** DNS; - the did:web port rule and no path-based did:web; - response-size caps (for example 64 KiB for DID documents) and short timeouts; - an explicit development opt-out for local or private PLC mirrors. Today the two DID resolvers disagree: the one the space server uses accepts any `host:port`, for example `did:web:internal.corp%3A6379`. - [ ] Handle resolution: DNS TXT (a configurable DoH endpoint; today it is hard-coded to `dns.google`) plus HTTPS well-known, keeping the current fail-closed-on-conflict semantics. ## Lexicon and PLC - [ ] Add `com.atproto.identity.resolveIdentity`, `resolveDid` and `refreshIdentity` (+ `defs#identityInfo`). - [ ] `PlcClient` takes an `HttpClient` plus an explicit directory URL (it relies on `BaseAddress` today). Keep the audit, log, data and health APIs and **add tests** for them. Add PLC `/export` and the `/export/stream` WebSocket. - [ ] Remove the duplicated PLC models (`PlcOperationService` = `DidService`, `PlcData` ⊂ `PlcOperation`, …). ## Acceptance - A table-driven SSRF policy test. - Cache tests (TTL, negative, single-flight, invalidate). - A firehose verification test showing zero network calls on cache hits. - CHANGELOG `Breaking changes` (moved and renamed types, the exception type), `Security` (SSRF), `Changed` (caching).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#120
No description provided.