From 365dfb9a1e986b68bc2389e2a3cd3da30b0d4636 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 20 Feb 2026 12:21:24 +0000 Subject: integrate co-maintainer announcement rationale into architecture docs adds scam-prevention rationale, announcement model trade-offs, and push/fetch asymmetry to docs/architecture/maintainer-model.md --- docs/architecture/maintainer-model.md | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) (limited to 'docs/architecture') diff --git a/docs/architecture/maintainer-model.md b/docs/architecture/maintainer-model.md index 9aebea7..54decb5 100644 --- a/docs/architecture/maintainer-model.md +++ b/docs/architecture/maintainer-model.md @@ -82,3 +82,93 @@ When `ngit init` runs, there are 5 possible states based on what exists locally | **Not Listed** | Announcement exists, I'm not in maintainer set | Requires `--force` | See `src/bin/ngit/sub_commands/init.rs` (`InitState` enum) and `tests/ngit_init.rs` for the implementation and test coverage. + +## Why Each Co-Maintainer Must Publish Their Own Announcement + +### The Scam Scenario + +Nostr git repository state is tracked by Kind:30618 events (the "git state" event). +These events say: "for the repository with identifier `X`, the current branch tips are...". + +Crucially, a state event knows only its identifier (`d` tag) — not which specific +coordinate chain (which trusted maintainer's pubkey) it belongs to. + +This creates an attack vector: + +1. Alice has a reputation in the Rust ecosystem. She contributes to `my-lib` and + is listed in the trusted maintainer's Kind:30617 announcement. +2. Alice pushes and, in doing so, publishes a Kind:30618 state event for + identifier `my-lib`. +3. A scammer creates a completely different, malicious repository — also with + identifier `my-lib` — and publishes their own Kind:30617 listing Alice as a + maintainer. +4. The scammer points to Alice's state event as "proof" that Alice maintains their + project. Clients that filter state events by maintainer pubkey would include + Alice's state event when fetching the scam repo. +5. Alice's reputation is attached to a project she has never heard of. + +### Why the Announcement Resolves This + +A Kind:30617 announcement is a signed statement from Alice that says: + +> "I, Alice (pubkey X), am a maintainer of the repository at identifier `my-lib` +> whose trusted maintainer is pubkey Y." + +The coordinate used to discover Alice's announcement is +`30617:Y:{identifier}` — it is rooted at the trusted maintainer's pubkey, not the +identifier alone. Alice's own announcement event, signed by Alice's key, is published +under that same coordinate chain (because `get_repo_ref_from_cache` walks the +maintainer graph starting from Y's event, and finds Alice's event because Alice +listed herself as a maintainer of the same identifier). + +A scammer's fake `my-lib` has a different trusted maintainer pubkey (Z, not Y). Even +if Z lists Alice in their maintainers tag, Alice's existing Kind:30617 under the +`30617:Y:my-lib` coordinate chain does NOT appear under `30617:Z:my-lib`. The scammer +cannot bootstrap from Alice's existing announcement. + +And crucially: Alice's Kind:30618 state events are only fetched when a client is +looking at a coordinate chain that Alice has explicitly announced. If Alice has never +published a Kind:30617 for `30617:Z:my-lib`, her state events are not fetched in +that context. + +Without Alice's announcement, her state events carry no coordinate chain membership. +With her announcement, her state events are trusted only within the chains she has +explicitly joined. + +### The Remaining Vulnerability Without Announcements + +If we allowed state events without announcements — i.e., we filtered state events +by `repo_ref.maintainers` even for maintainers without their own Kind:30617 — the +attack above works: + +- The scammer publishes `30617:Z:my-lib` listing Alice. +- `get_repo_ref_from_cache` for `Z`'s coordinate now includes Alice in `maintainers`. +- `get_filter_state_events` includes Alice's pubkey in the author filter. +- Alice's legitimate state events for the real `my-lib` are fetched and used by the + scam repo. + +The scammer cannot forge Alice's state events (they're signed), but they can attribute +real ones to their fake project. A user fetching the scam repo sees a real commit +history, ostensibly co-maintained by Alice, with no indication anything is wrong. + +### Asymmetric Enforcement: Push vs Fetch + +ngit enforces the announcement requirement on push only. When fetching, state events +are accepted from any pubkey in the maintainer set, regardless of whether that pubkey +has published its own Kind:30617. This encourages good practice while remaining +resilient when other tools don't follow the same pattern. + +- **Push (strict)**: ngit will not publish a state event for a co-maintainer who + lacks an announcement. If the user has no announcement, ngit auto-publishes one + with defaults before proceeding. This ensures that every state event ngit produces + is backed by an explicit, signed opt-in. + +- **Fetch (permissive)**: state events from announcement-less maintainers are still + accepted. This keeps ngit interoperable with other tools that may not enforce the + announcement requirement, and avoids silently dropping legitimate state from + maintainers who used a different client. + +The scam scenario is therefore partially mitigated rather than fully prevented: a +scammer can still attribute Alice's state events to a fake coordinate chain if Alice +has never pushed via ngit. The push-side requirement limits the window of exposure +to maintainers who have only ever used non-compliant tooling. -- cgit v1.2.3