diff options
Diffstat (limited to 'docs/architecture')
| -rw-r--r-- | docs/architecture/maintainer-model.md | 90 |
1 files changed, 90 insertions, 0 deletions
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 | |||
| 82 | | **Not Listed** | Announcement exists, I'm not in maintainer set | Requires `--force` | | 82 | | **Not Listed** | Announcement exists, I'm not in maintainer set | Requires `--force` | |
| 83 | 83 | ||
| 84 | See `src/bin/ngit/sub_commands/init.rs` (`InitState` enum) and `tests/ngit_init.rs` for the implementation and test coverage. | 84 | See `src/bin/ngit/sub_commands/init.rs` (`InitState` enum) and `tests/ngit_init.rs` for the implementation and test coverage. |
| 85 | |||
| 86 | ## Why Each Co-Maintainer Must Publish Their Own Announcement | ||
| 87 | |||
| 88 | ### The Scam Scenario | ||
| 89 | |||
| 90 | Nostr git repository state is tracked by Kind:30618 events (the "git state" event). | ||
| 91 | These events say: "for the repository with identifier `X`, the current branch tips are...". | ||
| 92 | |||
| 93 | Crucially, a state event knows only its identifier (`d` tag) — not which specific | ||
| 94 | coordinate chain (which trusted maintainer's pubkey) it belongs to. | ||
| 95 | |||
| 96 | This creates an attack vector: | ||
| 97 | |||
| 98 | 1. Alice has a reputation in the Rust ecosystem. She contributes to `my-lib` and | ||
| 99 | is listed in the trusted maintainer's Kind:30617 announcement. | ||
| 100 | 2. Alice pushes and, in doing so, publishes a Kind:30618 state event for | ||
| 101 | identifier `my-lib`. | ||
| 102 | 3. A scammer creates a completely different, malicious repository — also with | ||
| 103 | identifier `my-lib` — and publishes their own Kind:30617 listing Alice as a | ||
| 104 | maintainer. | ||
| 105 | 4. The scammer points to Alice's state event as "proof" that Alice maintains their | ||
| 106 | project. Clients that filter state events by maintainer pubkey would include | ||
| 107 | Alice's state event when fetching the scam repo. | ||
| 108 | 5. Alice's reputation is attached to a project she has never heard of. | ||
| 109 | |||
| 110 | ### Why the Announcement Resolves This | ||
| 111 | |||
| 112 | A Kind:30617 announcement is a signed statement from Alice that says: | ||
| 113 | |||
| 114 | > "I, Alice (pubkey X), am a maintainer of the repository at identifier `my-lib` | ||
| 115 | > whose trusted maintainer is pubkey Y." | ||
| 116 | |||
| 117 | The coordinate used to discover Alice's announcement is | ||
| 118 | `30617:Y:{identifier}` — it is rooted at the trusted maintainer's pubkey, not the | ||
| 119 | identifier alone. Alice's own announcement event, signed by Alice's key, is published | ||
| 120 | under that same coordinate chain (because `get_repo_ref_from_cache` walks the | ||
| 121 | maintainer graph starting from Y's event, and finds Alice's event because Alice | ||
| 122 | listed herself as a maintainer of the same identifier). | ||
| 123 | |||
| 124 | A scammer's fake `my-lib` has a different trusted maintainer pubkey (Z, not Y). Even | ||
| 125 | if Z lists Alice in their maintainers tag, Alice's existing Kind:30617 under the | ||
| 126 | `30617:Y:my-lib` coordinate chain does NOT appear under `30617:Z:my-lib`. The scammer | ||
| 127 | cannot bootstrap from Alice's existing announcement. | ||
| 128 | |||
| 129 | And crucially: Alice's Kind:30618 state events are only fetched when a client is | ||
| 130 | looking at a coordinate chain that Alice has explicitly announced. If Alice has never | ||
| 131 | published a Kind:30617 for `30617:Z:my-lib`, her state events are not fetched in | ||
| 132 | that context. | ||
| 133 | |||
| 134 | Without Alice's announcement, her state events carry no coordinate chain membership. | ||
| 135 | With her announcement, her state events are trusted only within the chains she has | ||
| 136 | explicitly joined. | ||
| 137 | |||
| 138 | ### The Remaining Vulnerability Without Announcements | ||
| 139 | |||
| 140 | If we allowed state events without announcements — i.e., we filtered state events | ||
| 141 | by `repo_ref.maintainers` even for maintainers without their own Kind:30617 — the | ||
| 142 | attack above works: | ||
| 143 | |||
| 144 | - The scammer publishes `30617:Z:my-lib` listing Alice. | ||
| 145 | - `get_repo_ref_from_cache` for `Z`'s coordinate now includes Alice in `maintainers`. | ||
| 146 | - `get_filter_state_events` includes Alice's pubkey in the author filter. | ||
| 147 | - Alice's legitimate state events for the real `my-lib` are fetched and used by the | ||
| 148 | scam repo. | ||
| 149 | |||
| 150 | The scammer cannot forge Alice's state events (they're signed), but they can attribute | ||
| 151 | real ones to their fake project. A user fetching the scam repo sees a real commit | ||
| 152 | history, ostensibly co-maintained by Alice, with no indication anything is wrong. | ||
| 153 | |||
| 154 | ### Asymmetric Enforcement: Push vs Fetch | ||
| 155 | |||
| 156 | ngit enforces the announcement requirement on push only. When fetching, state events | ||
| 157 | are accepted from any pubkey in the maintainer set, regardless of whether that pubkey | ||
| 158 | has published its own Kind:30617. This encourages good practice while remaining | ||
| 159 | resilient when other tools don't follow the same pattern. | ||
| 160 | |||
| 161 | - **Push (strict)**: ngit will not publish a state event for a co-maintainer who | ||
| 162 | lacks an announcement. If the user has no announcement, ngit auto-publishes one | ||
| 163 | with defaults before proceeding. This ensures that every state event ngit produces | ||
| 164 | is backed by an explicit, signed opt-in. | ||
| 165 | |||
| 166 | - **Fetch (permissive)**: state events from announcement-less maintainers are still | ||
| 167 | accepted. This keeps ngit interoperable with other tools that may not enforce the | ||
| 168 | announcement requirement, and avoids silently dropping legitimate state from | ||
| 169 | maintainers who used a different client. | ||
| 170 | |||
| 171 | The scam scenario is therefore partially mitigated rather than fully prevented: a | ||
| 172 | scammer can still attribute Alice's state events to a fake coordinate chain if Alice | ||
| 173 | has never pushed via ngit. The push-side requirement limits the window of exposure | ||
| 174 | to maintainers who have only ever used non-compliant tooling. | ||