From d089a4b0217ebf649c9406caaee9bdab2b58f2e7 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 25 May 2026 20:19:56 +0200 Subject: [PATCH] docs(spec): reframe identity model around operator-IdP + sidecar bunker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates §7.2, §7.3, §12 to reflect the actual architecture from aiolabs/lnbits#9 (reframed since the earlier commit) and #18 (the concrete phase 2 bunker integration using nsecbunkerd). Three shifts: - LocalSigner demoted to transitional/migration helper. RemoteBunker Signer is the steady state for every bound user. New accounts MUST NOT default to LocalSigner. Earlier framing treated them as equivalent choices — they're not. - Binding artifact is a per-device NIP-46 connection token with scoped permissions, not just a (mxid → user_id) mapping row. Calls out the security property: compromise of one client device (tracker, ATM, webapp) leaks only that token's scope, not the user's full identity. Revocation is one RPC at the bunker. - §12 redrawn around the operator-IdP-with-sidecar-bunker pattern. Names nsecbunkerd as the canonical bunker for the aiolabs ref impl, points at #9 + #18 for the LNbits side. Pattern is reusable beyond LNbits — any operator providing identity-as-a-service can run this shape. NIP-26 explicitly out (Nostr ecosystem has deprecated; NIP-46 covers the use case). §11 open questions trimmed accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/community-organizer-spec.md | 200 +++++++++++++++++++++++-------- 1 file changed, 152 insertions(+), 48 deletions(-) diff --git a/docs/community-organizer-spec.md b/docs/community-organizer-spec.md index da16075..4174e8d 100644 --- a/docs/community-organizer-spec.md +++ b/docs/community-organizer-spec.md @@ -453,57 +453,107 @@ Implementations MUST design their signing layer as an abstraction so per-user signing plugs in without refactor. Recommended interface (adopted from the LNbits reference implementation, see §12): -| Signer | Holds | Where it signs | -|---|---|---| -| `BotSigner` | One server-side keypair shared across all events | Server-side, in-process. The v1 fallback. | -| `LocalSigner` | A user's encrypted-at-rest keypair on the bot's host | Server-side, in-process, but per-user | -| `RemoteBunkerSigner` | A NIP-46 remote-signer connection (per-user) | RPC over relay (kind 24133) to the user's bunker | -| `ClientSideOnlySigner` | Nothing — sentinel meaning "user signs in their own client" | Not the bot; events for this user come in via subscription from elsewhere | +| Signer | Holds | Where it signs | Lifecycle role | +|---|---|---|---| +| `BotSigner` | One server-side keypair shared across all events | Server-side, in-process | The v1 fallback and the unbound-user fallback | +| `RemoteBunkerSigner` | A NIP-46 connection token + bunker URL + scoped perms (per-user) | RPC over relay (kind 24133) to a sidecar bunker | **Steady state** — what every bound user ends up on | +| `ClientSideOnlySigner` | Nothing — sentinel meaning "user signs in their own client" | Not the bot; events for this user come in via subscription from elsewhere | Sovereignty escape (NIP-41) — user took ownership | +| `LocalSigner` | A user's encrypted-at-rest keypair on the bot's host | Server-side, in-process | **Transitional** — migration helper only; never the long-term home for a key | + +**The steady-state architecture is the operator-IdP-with-sidecar-bunker +pattern.** The bot (or any other client of the identity provider) holds +zero nsec material; a separate bunker process on the same host (e.g. +`nak bunker`) holds every user's target key and signs on RPC. The bot +authenticates to the bunker with a per-user NIP-46 connection token +issued at binding time (§7.3), scoped to only the kinds the bot needs +to sign. Compromise of the bot leaks the scoped tokens; revoking them +at the bunker side is one RPC and doesn't disturb the target key or +any other client device the user has authorized. + +`LocalSigner` is included in the abstraction for two narrow reasons: +(a) it's how identity providers migrate existing plaintext-nsec rows +into the bunker safely, and (b) it lets adopters who don't (yet) run a +bunker fall back to encrypt-at-rest in-process signing. Once a bunker +is available, the migration drains every `LocalSigner` row into it and +flips them to `RemoteBunkerSigner`. New accounts MUST NOT default to +`LocalSigner`. For each captured message the bot: 1. Looks up the originating chat handle (e.g. MXID) in its binding - table (§7.3). -2. If bound and the signer is server-callable (`LocalSigner` / - `RemoteBunkerSigner`), signs as the user. Drop the `author` tag - (the pubkey is the attribution). -3. If unbound — or bound to `ClientSideOnlySigner` for a verb the - user must sign themselves — falls back to `BotSigner` with the - `author` tag (the v1 §7.1 behavior). + table (§7.3) — yielding a connection token + bunker URL + pubkey + (or `None` if unbound). +2. If bound and the signer is server-callable (`RemoteBunkerSigner` + or, transitionally, `LocalSigner`), signs as the user. Drop the + `author` tag — the pubkey is the attribution. +3. If unbound — or bound to `ClientSideOnlySigner` for a verb the user + must sign themselves — falls back to `BotSigner` with the `author` + tag (the v1 §7.1 behavior). This degrades gracefully: a community can run v1 indefinitely with -just `BotSigner`; users opt into sovereignty individually as they +just `BotSigner`; users opt into per-user signing individually as they bind their identity. -**NIP-46 is the recommended remote signer protocol.** It's what the -Nostr ecosystem is converging on for client-without-nsec flows, and -it works without browser extensions on iOS — important for any -Matrix client that runs on mobile. +**NIP-46 is the only remote signer protocol this spec recognizes.** +It's what the Nostr ecosystem has converged on for client-without-nsec +flows, works without browser extensions on iOS, and natively supports +the per-device scoped tokens that make multi-client identity safe. +NIP-26 delegation tokens are explicitly NOT used (the Nostr ecosystem +has deprecated NIP-26 and NIP-46 covers the same ground better). ### 7.3 External-identity binding (v2) For per-user signing to mean anything, the bot needs a verified -mapping from `chat handle → external identity → signer`. The binding +mapping from `chat handle → identity provider → signer`. The binding mechanism is implementation-defined but MUST be: - **Opt-in** per user. No silent association. - **Verifiable** — the binding proves the user controls both identities - (e.g. magic-link round-trip, signature challenge, NIP-39 external - identity proof). -- **Revocable** — user can unbind. Bot drops back to fallback. + (e.g. magic-link round-trip via the IdP's authenticated session, + signature challenge, NIP-39 external identity proof). +- **Revocable** — user can unbind. Bot drops back to `BotSigner` + fallback. Crucially: revocation MUST also revoke whatever per-device + signing capability the binding established (see below). + +#### Binding artifact (the important part) + +A binding is not just a row in a mapping table — it's a **per-device +NIP-46 connection token** scoped to exactly the kinds the bot needs to +sign on behalf of the user. The bot stores: + +- The user's pubkey (for display, addressing, ECDH) +- The bunker URL (transport: which relay channel the bunker listens on) +- The connection token (the credential to authenticate to the bunker) +- The granted permission scope (e.g. `sign_event:31922,31923,31925` + for a community-organizer bot — no kind-1 notes, no DMs, no profile + edits) + +Per-device-scoped tokens are the security property that makes +multi-client identity safe. If the bot is compromised, the attacker +can publish NIP-52 events as the user — annoying, but they can't +publish kind-1 notes, change the user's profile, send DMs, or do +anything else the user's other devices can. Revocation of the bot's +token at the bunker side is one RPC and doesn't disturb the target +key or any other authorized device. + +#### Identity-provider options The spec does not mandate a specific identity provider. Any system -that can answer "for chat handle X, what signer should I use?" works: +that can answer "for chat handle X, issue me a scoped connection +token to sign on their behalf" works: - A **Lightning/Nostr account system** (LNbits, Alby Hub, etc.) — the - reference implementation, since these already hold user pubkeys - and have an auth surface that can mediate the binding flow. + reference implementation. These already hold user pubkeys and have + an authenticated session surface that can mediate the binding flow. + Per the operator-IdP pattern (§12), the IdP itself doesn't hold the + nsec — a sidecar bunker does — and the IdP brokers token issuance + to that bunker. - A **standalone web app** with its own auth and a `bind chat handle` - flow. -- An **on-Nostr profile claim** — user publishes a kind 0 / 30311 / - similar with a chat-handle attestation; bot reads relay and + flow that issues NIP-46 connection tokens. +- An **on-Nostr profile claim** — user publishes a kind-0 / kind-10002 + / NIP-39 attestation linking the chat handle to a pubkey, with a + pre-issued bunker URL in their profile. Bot reads relay and cryptographically verifies the claim. -- A **NIP-39 external-identity proof** in the user's profile. Adopters without any of the above can stay on v1 (`BotSigner` only) — per-user signing is enhancement, not requirement. @@ -752,13 +802,14 @@ Not yet decided in this draft: - **Migration / weekly review.** Bullet-journal-style migration of stale open items into a new period. Needs a verb (`!migrate`?) and a state transition spec. -- **External signer story.** NIP-46 bunker integration is sketched - in §7.2 / §7.3; the LNbits reference identity provider is being - hardened in [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9). - Pending: ergonomics of the chat-side binding flow (DM the bot? web - callback? both?), and how to handle `ClientSideOnlySigner` users - whose events can't be bot-published at all (the bot subscribes and - mirrors instead). +- **External signer story.** Architecture is concrete in §7.2 / §7.3 + / §12 (operator-IdP with sidecar `nsecbunkerd`, per-device scoped + connection tokens). Phase 1 of [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9) + has shipped the signer abstraction; phase 2 ([#18](https://git.atitlan.io/aiolabs/lnbits/issues/18)) + is the actual bunker integration. Pending design: ergonomics of the + chat-side binding flow (DM the bot? web callback? both?), and how + to handle `ClientSideOnlySigner` users whose events can't be + bot-published at all (the bot subscribes and mirrors instead). Contributions welcome on any of these. @@ -791,20 +842,73 @@ scoping in §5. A ZeroClaw-based implementation would carry the `["client", "maubot-tracker", "..."]`; renderers ignore the difference since they filter by community `a`-tag. -### Reference identity provider +### Reference identity provider — operator-IdP pattern -The aiolabs reference implementation uses [LNbits](https://lnbits.com/) -(specifically the [aiolabs fork](https://git.atitlan.io/aiolabs/lnbits)) -as its identity provider — each user already has an LNbits account with -a Nostr `pubkey` field; the in-progress [issue #9 hardening](https://git.atitlan.io/aiolabs/lnbits/issues/9) -introduces a `NostrSigner` abstraction (`LocalSigner` / `RemoteBunkerSigner` -/ `ClientSideOnlySigner`) that matches the per-user-signing model in §7.2 -exactly. The bot stores `(chat_handle → lnbits_user_id)` and resolves the -signer per-user at publish time. +The aiolabs reference implementation runs the **operator-IdP-with- +sidecar-bunker** pattern that NIP-46 was designed for. Three processes +on the same host: -Other communities can substitute their own identity provider (any system -that maps chat handles to Nostr signers per §7.3) — LNbits is one such -provider, not the only one. +``` + ┌─────────────────────────────┐ + │ nsecbunkerd (sidecar) │ Holds every user's target key + │ │ + the operator master. + │ - admin: M_lnbits │ Per-device scoped connection + │ - targets: X_alice, … │ tokens. + │ - per-client tokens │ Speaks NIP-46 over kind-24133; + │ - scoped perms │ admin RPC over kind-24134. + └────────────┬────────────────┘ + │ kind-24133 / 24134 over internal relay + ▼ + ┌─────────────────────────────┐ + │ LNbits (identity broker) │ Holds zero nsec material. + │ │ Stores per-user (pubkey, + │ - account → pubkey │ signer_type, signer_config) + │ - account → bunker URL │ where signer_config is the + │ - admin RPC client │ bunker URL + connection token + │ (scoped agent key) │ + scoped perms. + └────────────┬────────────────┘ + │ HTTPS (token issuance, account lookup) + ▼ + ┌─────────────────────────────┐ + │ Bot / tracker plugin / │ Holds per-user connection + │ other clients │ tokens scoped to its needs. + │ │ Speaks NIP-46 directly to + │ - NIP-46 client per user │ the bunker. Compromise → + │ for sign_event RPCs │ revoke just that token. + └─────────────────────────────┘ +``` + +This is the architecture from [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9) +(the IdP framing and signer abstraction) and +[aiolabs/lnbits#18](https://git.atitlan.io/aiolabs/lnbits/issues/18) +(the concrete bunker integration using +[`nsecbunkerd`](https://github.com/kind-0/nsecbunkerd)). PR #17 has +shipped phase 1 (the abstraction + transitional `LocalSigner` + the +classify migration); phase 2 (#18) is the actual bunker integration. + +#### Why a sidecar bunker, not "encrypt at rest in the IdP" + +The IdP attack surface (web requests, dependency tree, plugin system, +admin endpoints, the database) is large. Putting nsec material on the +other side of an RPC boundary — held by a much smaller process whose +only job is signing — bounds compromise to "we leak signed events +during the window the bunker is unavailable for revocation" rather +than "the entire user identity pool is exfiltrated". The bunker also +gets its own audit log, independent of the IdP's. + +The pattern is reusable beyond LNbits — any operator who wants to +provide identity-as-a-service to their users can run this same shape +with any NIP-46-compliant bunker (`nsecbunkerd`, Pablo's reference +implementation, or any future alternative). The Nostriga 2024 NIP-46 +panel describes the same architecture. + +#### Adopters without an IdP + +Communities without an existing identity provider can run v1 +(`BotSigner` only) indefinitely. Per-user signing is enhancement, +not requirement — the community organizer use case works fine with +the bot signing as itself and human attribution carried in the +`author` tag. ---