atproto utils for zig
zat.dev
atproto
sdk
zig
1# verifying the trust chain
2
3since the last devlog (firehose benchmarks), zat picked up a bunch of correctness work — interop test suites, signature fixes, a full MST implementation — and now ties it all together: given a handle, verify everything about a repo from scratch.
4
5## what happened since last time
6
7### correctness first (0.1.8)
8
9we joined the [atproto interop test suite](https://github.com/bluesky-social/atproto-interop-tests). this is bluesky's official cross-implementation test vectors — the same fixtures that the TypeScript SDK, Go SDK, and others validate against. zat now passes all of them:
10
11- **syntax**: 6 types (TID, DID, Handle, NSID, RecordKey, AT-URI), valid + invalid vectors
12- **crypto**: 6 signature verification vectors (P-256 and secp256k1)
13- **MST**: 9 key height vectors, 13 common prefix vectors, 6 commit proof fixtures
14
15this also surfaced two bugs:
16- NSID parser wasn't rejecting TLDs starting with a digit (`1.0.0.127.record` should fail)
17- AT-URI parser wasn't validating its components (authority, collection, rkey) — it was just splitting on `/`
18
19and a spec compliance issue: ECDSA signature verification wasn't rejecting high-S values. atproto requires low-S normalization (BIP-62 style), and we were accepting both. fixed with explicit half-order checks in `verifyP256` and `verifySecp256k1`.
20
21### MST and crypto signing (0.1.9)
22
23the merkle search tree is the core data structure of an atproto repo. each key's tree layer is derived from the leading zero bits of SHA-256(key), and nodes are serialized with prefix compression. `mst.Mst` supports `put`, `get`, `delete`, and `rootCid` (serialize → hash → CID).
24
25alongside that: ECDSA signing (`signSecp256k1`, `signP256` with RFC 6979 deterministic nonces), `did:key` construction, and multibase encoding. these round out the crypto layer — zat can now both sign and verify.
26
27### code organization (0.2.0)
28
2922 files in a flat `src/internal/` was getting unwieldy. we reorganized into domain subdirectories following bluesky's own boundaries (from the [TypeScript SDK](https://github.com/bluesky-social/atproto/tree/main/packages)):
30
31```
32internal/
33 syntax/ — tid, did, handle, nsid, rkey, at_uri
34 crypto/ — jwt, multibase, multicodec
35 identity/ — did_document, did_resolver, handle_resolver
36 repo/ — cbor, car, mst, repo_verifier
37 xrpc/ — transport, xrpc, json
38 streaming/ — firehose, jetstream, sync
39 testing/ — interop_tests
40```
41
42the groupings aren't arbitrary. the TypeScript SDK has `syntax`, `crypto`, `identity`, `repo`, and `xrpc` as distinct packages — `syntax` is pure parsing with zero deps, `identity` handles network resolution, `crypto` is P-256 + K-256, and `repo` contains the MST, CAR, and CBOR together (CBOR isn't a standalone package — it lives with the types that need it).
43
44## the repo verifier
45
46`verifyRepo(allocator, "pfrazee.com")` exercises the entire trust chain in one call:
47
48```
49handle → DID → DID document → signing key
50 ↓
51repo CAR → commit → signature ← verified against key
52 ↓
53 MST root CID → walk nodes → rebuild tree → CID match
54```
55
56the pipeline:
57
581. **resolve handle** — HTTP well-known or DNS TXT → DID string
592. **resolve DID** — did:plc via plc.directory, did:web via .well-known/did.json → DID document
603. **extract signing key** — find the `#atproto` verification method, multibase decode, multicodec parse → key type + raw bytes
614. **extract PDS endpoint** — find the `#atproto_pds` service
625. **fetch repo** — HTTP GET `{pds}/xrpc/com.atproto.sync.getRepo?did={did}` → raw CAR bytes
636. **parse CAR** — extract roots and blocks
647. **find + decode commit** — the root block is the signed commit (DAG-CBOR map with `did`, `version`, `rev`, `data`, `sig`)
658. **verify signature** — strip `sig` from the commit map, re-encode to DAG-CBOR (deterministic key ordering), verify with the signing key
669. **walk MST** — starting from the commit's `data` CID, recursively decode MST nodes with prefix decompression, collect all (key, value_cid) pairs
6710. **rebuild MST** — insert every record into a fresh `mst.Mst`, compute root CID, compare against the commit's `data` CID
68
69if any step fails, you know exactly where the trust chain breaks.
70
71### what this exercises
72
73every major module in zat participates:
74
75| step | modules used |
76|------|-------------|
77| handle resolution | `HandleResolver`, `Handle` |
78| DID resolution | `DidResolver`, `Did`, `DidDocument` |
79| key extraction | `multibase`, `multicodec` |
80| HTTP fetch | `HttpTransport` |
81| repo parsing | `car`, `cbor` |
82| signature verification | `jwt.verifyP256` / `jwt.verifySecp256k1` |
83| MST walk + rebuild | `mst.Mst`, `cbor.Value` |
84
85it's the first feature that crosses all the domain boundaries — identity, crypto, repo, and network all working together.
86
87### the integration tests
88
89two accounts, two PDS backends:
90
91- **zzstoatzz.io** — self-hosted PDS (`pds.zzstoatzz.io`), ~12k records. verifies the self-hosting path works.
92- **pfrazee.com** — bluesky CTO, hosted on `bsky.network`, ~192k records. verifies against the canonical infrastructure.
93
94both use the graceful-catch pattern: if the network isn't available (CI, offline), the test prints a message and passes. when the network is there, it runs the full chain and asserts on the DID and record count.
95
96## what's next
97
98this is the first "full pipeline" feature — it validates that the primitives compose correctly end to end. from here, the natural next steps are incremental: repo diffing (compare two commits), record-level verification (check a specific record's inclusion proof), or sync protocol support.
99
100but following the pattern: we ship when something real needs it, not before.