docs(spec): reframe identity model around operator-IdP + sidecar bunker
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) <noreply@anthropic.com>
This commit is contained in:
parent
3d29beecba
commit
d089a4b021
1 changed files with 154 additions and 50 deletions
|
|
@ -453,57 +453,107 @@ Implementations MUST design their signing layer as an abstraction so
|
||||||
per-user signing plugs in without refactor. Recommended interface
|
per-user signing plugs in without refactor. Recommended interface
|
||||||
(adopted from the LNbits reference implementation, see §12):
|
(adopted from the LNbits reference implementation, see §12):
|
||||||
|
|
||||||
| Signer | Holds | Where it signs |
|
| Signer | Holds | Where it signs | Lifecycle role |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `BotSigner` | One server-side keypair shared across all events | Server-side, in-process. The v1 fallback. |
|
| `BotSigner` | One server-side keypair shared across all events | Server-side, in-process | The v1 fallback and the unbound-user 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 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 |
|
||||||
| `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 | Sovereignty escape (NIP-41) — user took ownership |
|
||||||
| `ClientSideOnlySigner` | Nothing — sentinel meaning "user signs in their own client" | Not the bot; events for this user come in via subscription from elsewhere |
|
| `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:
|
For each captured message the bot:
|
||||||
|
|
||||||
1. Looks up the originating chat handle (e.g. MXID) in its binding
|
1. Looks up the originating chat handle (e.g. MXID) in its binding
|
||||||
table (§7.3).
|
table (§7.3) — yielding a connection token + bunker URL + pubkey
|
||||||
2. If bound and the signer is server-callable (`LocalSigner` /
|
(or `None` if unbound).
|
||||||
`RemoteBunkerSigner`), signs as the user. Drop the `author` tag
|
2. If bound and the signer is server-callable (`RemoteBunkerSigner`
|
||||||
(the pubkey is the attribution).
|
or, transitionally, `LocalSigner`), signs as the user. Drop the
|
||||||
3. If unbound — or bound to `ClientSideOnlySigner` for a verb the
|
`author` tag — the pubkey is the attribution.
|
||||||
user must sign themselves — falls back to `BotSigner` with the
|
3. If unbound — or bound to `ClientSideOnlySigner` for a verb the user
|
||||||
`author` tag (the v1 §7.1 behavior).
|
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
|
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.
|
bind their identity.
|
||||||
|
|
||||||
**NIP-46 is the recommended remote signer protocol.** It's what the
|
**NIP-46 is the only remote signer protocol this spec recognizes.**
|
||||||
Nostr ecosystem is converging on for client-without-nsec flows, and
|
It's what the Nostr ecosystem has converged on for client-without-nsec
|
||||||
it works without browser extensions on iOS — important for any
|
flows, works without browser extensions on iOS, and natively supports
|
||||||
Matrix client that runs on mobile.
|
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)
|
### 7.3 External-identity binding (v2)
|
||||||
|
|
||||||
For per-user signing to mean anything, the bot needs a verified
|
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:
|
mechanism is implementation-defined but MUST be:
|
||||||
|
|
||||||
- **Opt-in** per user. No silent association.
|
- **Opt-in** per user. No silent association.
|
||||||
- **Verifiable** — the binding proves the user controls both identities
|
- **Verifiable** — the binding proves the user controls both identities
|
||||||
(e.g. magic-link round-trip, signature challenge, NIP-39 external
|
(e.g. magic-link round-trip via the IdP's authenticated session,
|
||||||
identity proof).
|
signature challenge, NIP-39 external identity proof).
|
||||||
- **Revocable** — user can unbind. Bot drops back to fallback.
|
- **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
|
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
|
- A **Lightning/Nostr account system** (LNbits, Alby Hub, etc.) — the
|
||||||
reference implementation, since these already hold user pubkeys
|
reference implementation. These already hold user pubkeys and have
|
||||||
and have an auth surface that can mediate the binding flow.
|
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`
|
- A **standalone web app** with its own auth and a `bind chat handle`
|
||||||
flow.
|
flow that issues NIP-46 connection tokens.
|
||||||
- An **on-Nostr profile claim** — user publishes a kind 0 / 30311 /
|
- An **on-Nostr profile claim** — user publishes a kind-0 / kind-10002
|
||||||
similar with a chat-handle attestation; bot reads relay and
|
/ 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.
|
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)
|
Adopters without any of the above can stay on v1 (`BotSigner` only)
|
||||||
— per-user signing is enhancement, not requirement.
|
— 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
|
- **Migration / weekly review.** Bullet-journal-style migration of
|
||||||
stale open items into a new period. Needs a verb (`!migrate`?)
|
stale open items into a new period. Needs a verb (`!migrate`?)
|
||||||
and a state transition spec.
|
and a state transition spec.
|
||||||
- **External signer story.** NIP-46 bunker integration is sketched
|
- **External signer story.** Architecture is concrete in §7.2 / §7.3
|
||||||
in §7.2 / §7.3; the LNbits reference identity provider is being
|
/ §12 (operator-IdP with sidecar `nsecbunkerd`, per-device scoped
|
||||||
hardened in [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9).
|
connection tokens). Phase 1 of [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9)
|
||||||
Pending: ergonomics of the chat-side binding flow (DM the bot? web
|
has shipped the signer abstraction; phase 2 ([#18](https://git.atitlan.io/aiolabs/lnbits/issues/18))
|
||||||
callback? both?), and how to handle `ClientSideOnlySigner` users
|
is the actual bunker integration. Pending design: ergonomics of the
|
||||||
whose events can't be bot-published at all (the bot subscribes and
|
chat-side binding flow (DM the bot? web callback? both?), and how
|
||||||
mirrors instead).
|
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.
|
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
|
`["client", "maubot-tracker", "..."]`; renderers ignore the difference
|
||||||
since they filter by community `a`-tag.
|
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/)
|
The aiolabs reference implementation runs the **operator-IdP-with-
|
||||||
(specifically the [aiolabs fork](https://git.atitlan.io/aiolabs/lnbits))
|
sidecar-bunker** pattern that NIP-46 was designed for. Three processes
|
||||||
as its identity provider — each user already has an LNbits account with
|
on the same host:
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue