Skip to content

Add AtprotoTypesVerify: CAR/MST/CID repo-proof verification - #54

Merged
germ-mark merged 9 commits into
mainfrom
llm/atproto-repo-verify
Aug 18, 2026
Merged

Add AtprotoTypesVerify: CAR/MST/CID repo-proof verification#54
germ-mark merged 9 commits into
mainfrom
llm/atproto-repo-verify

Conversation

@germ-mark

@germ-mark germ-mark commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Relocates GER-2254's CAR/DAG-CBOR/MST/CID/commit-signature verifier from AtprotoGerm (germDM-ios-refresh) to here, where it belongs: it's generic com.atproto.repo protocol machinery with zero Germ-specific coupling, sitting beside Atproto.CID/DID/DIDDocument rather than beside Germ's declaration/anchoring logic.

Two additive products, no changes to the existing AtprotoTypes target's public API:

  • AtprotoTypesVerify — the verifier itself. Atproto.Repo.RecordPath/Proof/ProofVerifying/ProofUnavailable move into AtprotoTypes core (the Clip-safe seam); CAR/DAG-CBOR/MST/CID/RepoSigningKey/Verifier land in the new product. No new third-party dependencies — swift-crypto and swift-bases are already package-level dependencies here.
  • AtprotoTypesVerifyMocks — promotes the fixture-building test support (RepoFixture: builds a real signed commit over an MST over record blocks) into a real library product, mirroring AtprotoTypesMocks. Needed because a consuming package's tests can't reach @testable-only internals across a package boundary — two members (RepoSigningKey.p256Order/.isLowS) went from internal to public for this reason. Its signer is now a protocol (RepoFixtureSigningKey) rather than concrete P-256, so a consumer's tests can build synthetic k256 repos too.

secp256k1 is supported, not just P-256: a from-scratch, verify-only Swift port (field/scalar arithmetic on UInt64 limbs — no UInt128, this package floors below its availability; Jacobian point operations; SEC1 decompression; ECDSA), since most real Bluesky accounts sign with this curve and swift-crypto has no k256 support at all. Checked against Wycheproof's 252-vector secp256k1 suite (exact bucket census: 95 accept / 116 refused-non-canonical / 18 refused-bad-length / 23 refused-other, not just "no crash") and a P256K-backed differential oracle (100 keypairs × 3 messages, plus mutation-agreement tests) — both test-only, swift-secp256k1 never ships in a product. A genuine k256 commit signature verifies end-to-end through the whole CAR/MST/commit-signature pipeline.

Why these aren't imported

docs/dependency-choices.md records the evaluation for all five primitives — which libraries were considered, the specific property each can't express, and what would change the answer.

Worth flagging for review, since it's counterintuitive: strictness is no longer the reason for the DAG-CBOR decision. swift-cbor 0.1.0 does enforce a real DAG-CBOR profile (and is already in the wider dependency graph, so it'd be free), as does thecoolwinter/CBOR. The blocker is that both are Codable-only with no public value tree — and verifying a commit signature means re-encoding the commit minus its sig, so any field a struct doesn't model would vanish from the preimage and break every proof the first time a PDS writes something unanticipated.

Testing

81 tests in 10 suites (AtprotoTypesVerifyTests), existing AtprotoTypesTests unaffected. DAG-CBOR is pinned against the RFC 8949 vector table; fixtures build real repos so a test can forge one field and watch the proof fail for that reason.

🤖 Generated with Claude Code

Belongs here, not downstream in a consumer package: this is generic
com.atproto.repo protocol machinery (CAR framing, DAG-CBOR, MST proof
walking, CID recomputation, commit-signature verification) with zero
domain-specific coupling, sitting beside Atproto.CID/DID/DIDDocument
the same way Atproto.Repo already reads.

Two additive products so a space-constrained consumer (e.g. an App
Clip target) can link only the light seam (RecordPath/Proof/
ProofVerifying/ProofUnavailable, now in AtprotoTypes core) and never
pay for AtprotoTypesVerify existing. AtprotoTypesVerifyMocks promotes
the fixture-building test support into a real library product,
mirroring AtprotoTypesMocks, since a different package's tests can't
reach @testable-only internals across a package boundary.

secp256k1 lands separately; a k256 account fails closed with
unsupportedCurve rather than being waved through.
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1359c2e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@germ-network/atprototypes Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

germ-mark and others added 7 commits August 17, 2026 15:40
Adversarial review found a real gap: RepoSigningKey.init(atprotoKeyIn:did:)
skipped its document-to-DID binding check entirely whenever a
verification method's controller field was empty — the common case
for real documents, not an edge case. Nothing else in the verifier
compares document.id to the did being checked, so a caller-side bug
pairing the wrong document with a did would have passed silently.
Fixed by falling back to document.id (DID convention: absent
controller means self-controlled) rather than skipping the check.
Mutation-verified: reverting the fix makes the new test fail, exactly
on this scenario.

Also: p256Order/isLowS/varint were public with no cross-package
consumer (AtprotoTypesVerifyMocks is in the same package) — tightened
to package. ContentIdentifier.atprotoCID round-tripped through the
base32 string form with a comment claiming Atproto.CID's byte init
was package-scoped to a different package; it's the same package now,
so it calls the package init directly and the property is no longer
throws. Changeset corrected — it claimed the core AtprotoTypes target
was untouched, but the Atproto.Repo seam landed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI invokes `xcodebuild -scheme AtprotoTypes ... test`, and the shared
scheme's TestAction only listed AtprotoTypesTests. The 46 verify
tests this branch added have never run in CI — only locally. Found
by adversarial review; confirmed by running the exact CI command
before and after (33 tests only, then 33 + 46 across both targets,
TEST SUCCEEDED).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ycheproof gate

From-scratch, verify-only Swift port (no UInt128, this package floors below
its availability) — most Bluesky accounts sign with this curve and
swift-crypto has no k256 support at all. Checked against Wycheproof's
252-vector secp256k1 test suite (exact bucket census, not just "no crash")
and a P256K-backed differential oracle in AtprotoTypesVerifyTests only —
nothing new ships in the product.

RepoFixture's signer is now a protocol (RepoFixtureSigningKey) instead of
concrete P256, so AtprotoTypesVerifyMocks can build synthetic k256 repos
without depending on a signing library itself; the actual k256 test signer
lives in AtprotoTypesVerifyTests, backed by P256K.
…dor license

- ECDSA.verify now rejects a non-32-byte digest explicitly. Scalar's
  reducingBigEndian init silently folds any other length to zero, and z = 0
  lets anyone forge a signature over an arbitrary (r, s) without the private
  key. Not reachable today (RepoSigningKey.verify always passes a SHA-256
  output), but ECDSA.verify is the boundary where that stops being true by
  construction rather than by caller discipline.
- Secp256k1FieldTests' ">p" case was missing a hex digit, so it exercised the
  length check rather than the range check it claims to.
- Vendor Wycheproof's LICENSE alongside the JSON fixture (Apache-2.0 requires
  it accompany redistribution).
- Correct two reduction-convergence doc comments that overstated how fast the
  fold shrinks per pass — conclusions were right, the sketched numbers weren't.
Adds docs/dependency-choices.md covering DAG-CBOR, CID, CAR, MST and
secp256k1: which libraries were evaluated, the specific property each one
can't express, and what would change the answer. The CBOR case in particular
is easy to get wrong from stale information — swift-cbor does enforce
DAG-CBOR strictness now; the blocker is that it and its alternatives are
Codable-only, and re-encoding a sig-stripped commit preimage needs a
schemaless tree or unknown fields vanish.

Each implementation file gets a one-line pointer rather than a copy of the
reasoning.

Also drops two references to a private tracker ID from this public repo's
source, which read as dead links to anyone outside the org.
The k256 differential-oracle dependency carries a `.buildTool()` plugin that
copies its shared sources into the build directory. xcodebuild refuses to run
an unvalidated plugin non-interactively, so every Apple job failed at
"Plugin SharedSourcesPlugin must be enabled before it can be used" while
Linux and Android stayed green — `swift test` doesn't gate plugins, which is
also why local runs missed it.

The workflow already passes -skipMacroValidation for the same class of
non-interactive gate; this adds the plugin equivalent.
@germ-mark
germ-mark merged commit 8a0248f into main Aug 18, 2026
6 checks passed
@germ-mark
germ-mark deleted the llm/atproto-repo-verify branch August 18, 2026 22:24
@github-actions github-actions Bot mentioned this pull request Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant