{
  "$type": "site.standard.document",
  "path": "/devlog/003",
  "publishedAt": "2026-05-25T15:56:58Z",
  "site": "at://did:plc:mkqt76xvfgxuemlwlx6ruc3w/site.standard.publication/3khuwc44c2256",
  "textContent": "# verifying the trust chain\n\nsince 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.\n\n## what happened since last time\n\n### correctness first (0.1.8)\n\nwe 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:\n\n- **syntax**: 6 types (TID, DID, Handle, NSID, RecordKey, AT-URI), valid + invalid vectors\n- **crypto**: 6 signature verification vectors (P-256 and secp256k1)\n- **MST**: 9 key height vectors, 13 common prefix vectors, 6 commit proof fixtures\n\nthis also surfaced two bugs:\n- NSID parser wasn't rejecting TLDs starting with a digit (`1.0.0.127.record` should fail)\n- AT-URI parser wasn't validating its components (authority, collection, rkey) — it was just splitting on `/`\n\nand 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`.\n\n### MST and crypto signing (0.1.9)\n\nthe 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).\n\nalongside 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.\n\n### code organization (0.2.0)\n\n22 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)):\n\n```\ninternal/\n  syntax/     — tid, did, handle, nsid, rkey, at_uri\n  crypto/     — jwt, multibase, multicodec\n  identity/   — did_document, did_resolver, handle_resolver\n  repo/       — cbor, car, mst, repo_verifier\n  xrpc/       — transport, xrpc, json\n  streaming/  — firehose, jetstream, sync\n  testing/    — interop_tests\n```\n\nthe 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).\n\n## the repo verifier\n\n`verifyRepo(allocator, \"pfrazee.com\")` exercises the entire trust chain in one call:\n\n```\nhandle → DID → DID document → signing key\n                                    ↓\nrepo CAR → commit → signature ← verified against key\n                ↓\n         MST root CID → walk nodes → rebuild tree → CID match\n```\n\nthe pipeline:\n\n1. **resolve handle** — HTTP well-known or DNS TXT → DID string\n2. **resolve DID** — did:plc via plc.directory, did:web via .well-known/did.json → DID document\n3. **extract signing key** — find the `#atproto` verification method, multibase decode, multicodec parse → key type + raw bytes\n4. **extract PDS endpoint** — find the `#atproto_pds` service\n5. **fetch repo** — HTTP GET `{pds}/xrpc/com.atproto.sync.getRepo?did={did}` → raw CAR bytes\n6. **parse CAR** — extract roots and blocks\n7. **find + decode commit** — the root block is the signed commit (DAG-CBOR map with `did`, `version`, `rev`, `data`, `sig`)\n8. **verify signature** — strip `sig` from the commit map, re-encode to DAG-CBOR (deterministic key ordering), verify with the signing key\n9. **walk MST** — starting from the commit's `data` CID, recursively decode MST nodes with prefix decompression, collect all (key, value_cid) pairs\n10. **rebuild MST** — insert every record into a fresh `mst.Mst`, compute root CID, compare against the commit's `data` CID\n\nif any step fails, you know exactly where the trust chain breaks.\n\n### what this exercises\n\nevery major module in zat participates:\n\n| step | modules used |\n|------|-------------|\n| handle resolution | `HandleResolver`, `Handle` |\n| DID resolution | `DidResolver`, `Did`, `DidDocument` |\n| key extraction | `multibase`, `multicodec` |\n| HTTP fetch | `HttpTransport` |\n| repo parsing | `car`, `cbor` |\n| signature verification | `jwt.verifyP256` / `jwt.verifySecp256k1` |\n| MST walk + rebuild | `mst.Mst`, `cbor.Value` |\n\nit's the first feature that crosses all the domain boundaries — identity, crypto, repo, and network all working together.\n\n### the integration tests\n\ntwo accounts, two PDS backends:\n\n- **zzstoatzz.io** — self-hosted PDS (`pds.zzstoatzz.io`), ~12k records. verifies the self-hosting path works.\n- **pfrazee.com** — bluesky CTO, hosted on `bsky.network`, ~192k records. verifies against the canonical infrastructure.\n\nboth 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.\n\n## what's next\n\nthis 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.\n\nbut following the pattern: we ship when something real needs it, not before.\n",
  "title": "verifying the trust chain"
}