~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Hosts identity data names are asked only on https, at public addresses

Merged
Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:core/identity-guard

What changes

One policy, in grain::guard. The rule backfill had for itself now lives in grain's core. A host that identity data names is asked only over https and only at a public address. Its hostname has to resolve to public addresses (PublicOnly, a reqwest DNS resolver), and no redirect is followed. public, may_ask, PublicOnly and client keep their per-range unit tests.

Ctx carries a guarded client. Ctx now holds guarded beside http, plus Ctx::may_ask. Identity data drives these fetches, and all of them go through the guard:

  • did:web documents. This includes auth::verify, which reads one before it checks any signature, so any token naming did:web:<anything> used to make grain-serve fetch that host.
  • /.well-known/atproto-did handle lookups.
  • The PDS a DID document names. A new identity::pds, called from resolve_pds, refuses one that is not askable.
  • indexRecords' read-back (sync::read_back). It checks the PDS it is given and asks it through the guarded client. When the caller's DID document names a PDS that is not askable, indexRecords answers 400 InvalidRequest, not 502.

Backfill uses the shared policy and drops its own copy of it, along with Config::private.

GRAIN_ALLOW_PRIVATE=1 (Env::allow_private) replaces GRAIN_BACKFILL_PRIVATE and lifts the policy for tests and dev. The harness contexts in crates/grain/tests/common set it, because the reference PDS runs over http on localhost.

The PLC directory, the configured handle resolvers (GRAIN_HANDLE_RESOLVER's XRPC services), GRAIN_SERVICE, the Jetstream and backfill's directories and mirror are settings, so they are not checked. They still go through Ctx::http, or backfill's own client as before.

Why

Since social.grain.sync.indexRecords landed, anyone with a service token can make grain-serve fetch a URL of their choosing: a DID document can name any PDS, and a did:web can name any host. Putting the policy in one place means every fetch driven by identity data gets the same rule.

knotless has no equivalent: its identity resolution uses one plain client. So this follows backfill's version of the policy, not knotless's.

How it was checked

All of it is left to CI; nothing was built or run locally. New tests:

  • guard unit tests per address range. Another test shows a guarded client refusing localhost.
  • identity refuses a did:web on loopback (both http and https), a handle over http, and http or private-address PDS endpoints.
  • indexRecords refuses a caller whose DID document names the reference PDS (loopback, http) when the override is off. read_back refuses https://127.0.0.1 by itself, and https://localhost at DNS.
  • Backfill's existing refusal test now runs with GRAIN_ALLOW_PRIVATE=0.

Shortcomings

  • No redirects are followed for handle lookups, did:web documents or PDS calls, so a handle whose /.well-known/atproto-did redirects no longer resolves over HTTPS. It can still resolve through the XRPC resolver.
  • The command line's own session calls and OAuth discovery for its own account still use Ctx::http. Their PDS URL has passed may_ask, but their hostname is not DNS-checked.
  • Backfill retries a did:web it refuses rather than giving up on it at once.

🤖 Generated with Claude Code

Approved

This pull moves backfill's fetch policy into grain::guard and applies it to every fetch that identity data drives. Those are did:web documents (including the one auth::verify reads), handle /.well-known lookups, the PDS a DID document names, and indexRecords' read-back. It follows the code I read: Ctx::may_ask is called before each guarded request, guarded has redirects off and the public-only resolver, and GRAIN_ALLOW_PRIVATE lifts both consistently. I found no other identity-driven fetch still on ctx.http. The remaining ctx.http uses are settings or the command line's own account, which the body already lists as shortcomings.

The map from Refused to 400 InvalidRequest in indexRecords is correct, and tests cover it. Backfill's old tests moved with the code, and the GRAIN_BACKFILL_PRIVATE references are gone. The check workflow passed. rust and harness were still running when I looked, and test was queued. Merge only once they pass, since nothing was run locally.

Nothing blocks the merge.

Merged as 9272691
Part of a stack of 2
  1. Hosts identity data names are asked only on https, at public addresses Landed as 9272691
  2. main