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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Core resolves identities, verifies atproto keys and calls XRPC

Merged
Merged grain.social asked to merge into feature/rust from grain.social/grain:core/identity

What changes

Keys and JWTs (crypto): secp256k1 and P-256 public keys as multikeys, did:keys and DID document verification methods (the older EcdsaSecp256*VerificationKey2019 types too); low-S signature checks; P-256 signing keys with their JWK; compact JWTs signed with ES256, and verified against a key whose alg they must name, with exp checked.

Identity (identity): handle to DID (/.well-known/atproto-did, then com.atproto.identity.resolveHandle on the Bluesky appview, as GRAIN_HANDLE_RESOLVER orders them), DID to document (PLC directory or did:web), the #atproto_pds endpoint and the #atproto signing key, and a DID's handle only once it resolves back. Identities and handles are cached on disk under GRAIN_CACHE_DIR; DID documents in memory for five minutes. A PDS that answers that an account is not there marks the identity cached as on it stale, so an account that moved PDS is looked up again.

XRPC (xrpc): queries, procedures (JSON, none, or raw bytes), uploadBlob, getBlob and com.atproto.space.getBlob (the repo named repo), with refusals as XrpcError (status, error, message, RateLimit-Reset). Auth is a bearer token or fixed headers; DPoP comes with OAuth in the next pull.

Underneath: env (the GRAIN_* settings these read, and the one shared HTTP client), keys (the clock and TIDs), store (JSON files written through a rename, private when asked), and telemetry (each outgoing call logged at debug, or warn when it fails or takes a second, never its query or headers, with the trace handed on as traceparent).

Why

grain-serve, the grain CLI and later MCP all resolve identities and call PDSes; this is the first of the two pulls that bring knotless's protocol modules into the core, the second being OAuth, sessions and service auth on top of these.

Taken from knotless's crates/knotless/src and cut to the protocol:

  • crypto, identity, xrpc: as they are, less kem_key (the #atproto_kem key a space-syncing service agrees a secret with; grain reads member repos directly) and with BlobRef moved into xrpc from knotless's records.
  • env: only the cache directory, PLC URL, handle resolvers, did:web scheme, extra CA certificates, a fixed PDS, identity TTL and timeout. Pack sizes, compaction, claims, appview, moderator, admin and bot DIDs, mirrors, the run SHA and update checks are knotless's. The config directory, identifier, password and browser come with the session pull, which reads them.
  • keys: the clock and TIDs; every ref, claim, check, pack and release key is the forge's.
  • telemetry: outgoing calls and traces only. Its /metrics counters, per-minute summaries, served-request lines, git and SQL lines, panic hook and filter reloading are left for when grain-serve has the requests to count; the slow threshold is a constant second rather than KNOTLESS_SLOW_MS.
  • store: as it is.

How it was checked

Nothing was built or run here beyond cargo fmt and cargo update -p grain; the rust check builds and tests it. knotless's unit tests come along where their code did, with the variables renamed. Added: ES256K JWTs, as most PDSes sign service tokens; a did:web account resolved end to end against local servers (handle, document, PDS, signing key, verified handle, the disk cache, forgetting); document URLs; GRAIN_SERVICE; XRPC calls, blobs, empty answers and refusals against a local server; the env defaults and overrides.

Shortcomings

No test talks to a real PDS or PLC directory: grain has no PDS harness yet, which is a later pull. Untested for that reason: PLC resolution against a directory, resolveHandle against an appview, and every XRPC method against a PDS's real answers (uploadBlob, getBlob, space.getBlob), all of which the local servers stand in for only as far as the request's shape.

Handles are resolved by HTTPS well-known and then the Bluesky appview, as knotless does; there is no DNS _atproto TXT lookup, so a handle with only a TXT record resolves through the appview.

Review

Start at identity.rs, then xrpc.rs. crypto.rs is knotless's with comments adjusted and one test added.

🤖 Generated with Claude Code

grain.social added 1 commit
Approved

This pull brings knotless's protocol modules into the grain core: keys and JWTs, handle/DID/PDS resolution with its caches, the XRPC client, and the env, store and telemetry that sit under them.

I read identity.rs, xrpc.rs and the JWT verification in crypto.rs closely. I did not read env, store, telemetry or the tests.

I found nothing that must change.

  • JWT verification requires alg to match the key, checks the signature, and rejects a missing or past exp.
  • Handle resolution validates names before any lookup.
  • Lookups at hosts the name points to are capped at 3 seconds.
  • A handle is returned only if it resolves back to the DID.
  • Refusals map cleanly to XrpcError.

Two notes, neither blocking:

  • resolve and handle read the cache file, change it, and write it back. Two concurrent lookups can overwrite each other's entry. This costs an extra lookup later, not a wrong answer.
  • document_url turns only %3A into : for did:web. A did:web with path segments (did:web:host:user:alice) would build a bad URL and fail to resolve. Atproto uses host-only did:web, so this is fine for now.

The rust, check and test checks were still running or queued when I looked. Merge only once they pass, since the description says nothing was built or run locally.

grain.social added 1 commit
Approved

The only change since my last round is in a test helper in xrpc.rs: it drops the unused Read import. My earlier review asked for nothing to change, and I found nothing new. The two earlier notes still apply and neither blocks the merge: concurrent cache writes can overwrite each other, and did:web with path segments isn't handled. The rust, check and test checks were still running or queued when I looked, so merge only once they pass. The description says nothing was built here.

Both notes are left as knotless has them, on purpose. The identity cache's read-modify-write can lose a concurrent entry, which costs a lookup and never a wrong answer; grain-serve's identities will live in its index rather than this file, so the file is the command line's, where lookups rarely race. did:web with path segments is not one atproto allows (host-only, as the spec says), so it failing to resolve is the right answer.

Merged as ebd6d92
Part of a stack of 2
  1. Core resolves identities, verifies atproto keys and calls XRPC Landed as ebd6d92
  2. main