S2 — NIP-46 connection-token enforcement on kind-21000 RPC (was: NIP-26 delegation) #16

Closed
opened 2026-05-15 18:08:25 +00:00 by padreug · 1 comment
Owner

Part of #13. Closes gaps G3 (rest, after S0) and G7 (no signed-request primitive). Primary work is in aiolabs/lnbits nostr-transport — this issue tracks it from satmachineadmin's side.

2026-05-26 — pivot from NIP-26 to NIP-46

NIP-26 has been explicitly removed from the lnbits security plan. From aiolabs/lnbits#9:

"NIP-26 delegation. The Nostr ecosystem has officially deprecated NIP-26 (unrecommended: adds unnecessary burden for little gain — top of the NIP-26 spec). NIP-46 supersedes the use case. We do not use delegation tags anywhere."

The replacement primitive is NIP-46 connection tokens issued by a sidecar nsecbunkerd. See aiolabs/lnbits#18 §F — the satmachineadmin ATM is called out by name as the canonical test case:

"Use nsecbunkerd's create_new_token admin RPC with a SigningCondition scoping the token to specific event kinds. For a satmachine ATM... perms=[sign_event:21000] ... 30-day expiry."

So S2 is no longer "validate a NIP-26 delegation tag on every event." It's "verify the inbound kind-21000 came from a sender pubkey whose create_new_token credential is still live in the bunker."

What the handler must do (revised)

For every inbound kind-21000:

  1. Verify outer Schnorr sig (NIP-01) — already done.
  2. Verify NIP-44 v2 MAC, decrypt — already done.
  3. Check ["expiration"] tag (NIP-40) — S1.
  4. Resolve sender_pubkey → bunker token record. The handler asks the bunker (or a cache populated from the bunker) whether sender_pubkey is a valid, unrevoked, unexpired connection token authorized to call sign_event:21000.
  5. Resolve the token → the target operator pubkey (X_alice, the user's LNbits account identity).
  6. Persist operator_pubkey onto Payment.extra so satmachineadmin can resolve the operator at settlement-land time without re-walking the chain. (Depends on S5.)

The ATM is therefore a NIP-46 client connecting through its scoped token, not a delegated signer. It signs kind-21000 with its own ephemeral pubkey (the token's client identity), and the bunker enforces the kind/expiry policy at the LNbits-handler boundary.

Why this is simpler than NIP-26 turned out to be

  • No delegation tag parsing / Schnorr-over-conditions verification on every event.
  • Revocation is bunker-side (revoke_user / future revoke_token); LNbits queries the bunker directly. No "wait for the old token's created_at < window to close" semantics.
  • Permission scope is already a Prisma row in nsecbunkerd (SigningCondition); we don't invent a new format.
  • Same wire transport as everything else in lnbits-nostr (kind-24133/24134 events over NIP-44 v2 on the internal relay).

Changes in this repo (unchanged in shape)

After the handler lands operator_pubkey on Payment.extra:

  • tasks.py:_handle_payment reads payment.extra["operator_pubkey"], matches it against machine.operator_user_id (via lnbits.core.crud.get_account_by_pubkey), and refuses to record a settlement if the chain doesn't close.
  • Defence in depth on operator identity at land time, not just at provisioning time.

Acceptance (revised)

  • Send a kind-21000 from a revoked token → handler rejects.
  • Send a kind-21000 from an expired token → handler rejects.
  • Send a kind-21000 from a token scoped to sign_event:1 (not 21000) → handler rejects.
  • Send a kind-21000 from an ATM whose token's target operator pubkey doesn't own the destination wallet → satmachineadmin refuses to record.
  • Healthy production path: bunker round-trip is cacheable so per-event latency stays sub-ms after warm-up.

Sequencing

Blocked on aiolabs/lnbits#18 — the bunker integration and per-device token plumbing must land first. Until then, S2 work in this repo can't move. The S2 effort in satmachineadmin shrinks correspondingly: read operator_pubkey off Payment.extra and join against the machine row. The hard work moves into the lnbits bunker client.

Reference

  • aiolabs/lnbits#9 — operator-IdP framing (reframed 2026-05-25).
  • aiolabs/lnbits#18 — sidecar bunker integration; §F is the satmachineadmin alignment.
  • NIP-46 spec: ~/dev/nostr-protocol/nips/46.md.
  • Design doc: docs/security-pathway-v1.md §5.1, §6.S2 — needs a follow-up edit reflecting this pivot.
  • Previous framing (NIP-26): preserved here in git history for context; not the path forward.
Part of #13. Closes gaps G3 (rest, after S0) and G7 (no signed-request primitive). **Primary work is in `aiolabs/lnbits` nostr-transport — this issue tracks it from satmachineadmin's side.** ## 2026-05-26 — pivot from NIP-26 to NIP-46 NIP-26 has been **explicitly removed** from the lnbits security plan. From `aiolabs/lnbits#9`: > *"NIP-26 delegation. The Nostr ecosystem has officially deprecated NIP-26 (unrecommended: adds unnecessary burden for little gain — top of the NIP-26 spec). NIP-46 supersedes the use case. We do not use delegation tags anywhere."* The replacement primitive is **NIP-46 connection tokens** issued by a sidecar `nsecbunkerd`. See `aiolabs/lnbits#18` §F — the satmachineadmin ATM is called out by name as the canonical test case: > *"Use nsecbunkerd's `create_new_token` admin RPC with a `SigningCondition` scoping the token to specific event kinds. For a satmachine ATM... `perms=[sign_event:21000]` ... 30-day expiry."* So S2 is no longer "validate a NIP-26 delegation tag on every event." It's "verify the inbound kind-21000 came from a sender pubkey whose `create_new_token` credential is still live in the bunker." ## What the handler must do (revised) For every inbound kind-21000: 1. Verify outer Schnorr sig (NIP-01) — already done. 2. Verify NIP-44 v2 MAC, decrypt — already done. 3. Check `["expiration"]` tag (NIP-40) — S1. 4. **Resolve `sender_pubkey` → bunker token record.** The handler asks the bunker (or a cache populated from the bunker) whether `sender_pubkey` is a *valid, unrevoked, unexpired* connection token authorized to call `sign_event:21000`. 5. Resolve the token → the *target* operator pubkey (`X_alice`, the user's LNbits account identity). 6. Persist `operator_pubkey` onto `Payment.extra` so satmachineadmin can resolve the operator at settlement-land time without re-walking the chain. (Depends on S5.) The ATM is therefore a **NIP-46 client connecting through its scoped token**, not a delegated signer. It signs kind-21000 with its *own* ephemeral pubkey (the token's client identity), and the bunker enforces the kind/expiry policy at the LNbits-handler boundary. ## Why this is simpler than NIP-26 turned out to be - No delegation tag parsing / Schnorr-over-conditions verification on every event. - Revocation is bunker-side (`revoke_user` / future `revoke_token`); LNbits queries the bunker directly. No "wait for the old token's `created_at <` window to close" semantics. - Permission scope is already a Prisma row in nsecbunkerd (`SigningCondition`); we don't invent a new format. - Same wire transport as everything else in lnbits-nostr (`kind-24133/24134` events over NIP-44 v2 on the internal relay). ## Changes in this repo (unchanged in shape) After the handler lands `operator_pubkey` on `Payment.extra`: - `tasks.py:_handle_payment` reads `payment.extra["operator_pubkey"]`, matches it against `machine.operator_user_id` (via `lnbits.core.crud.get_account_by_pubkey`), and refuses to record a settlement if the chain doesn't close. - Defence in depth on operator identity *at land time*, not just at provisioning time. ## Acceptance (revised) - [ ] Send a kind-21000 from a revoked token → handler rejects. - [ ] Send a kind-21000 from an expired token → handler rejects. - [ ] Send a kind-21000 from a token scoped to `sign_event:1` (not 21000) → handler rejects. - [ ] Send a kind-21000 from an ATM whose token's *target* operator pubkey doesn't own the destination wallet → satmachineadmin refuses to record. - [ ] Healthy production path: bunker round-trip is cacheable so per-event latency stays sub-ms after warm-up. ## Sequencing **Blocked on `aiolabs/lnbits#18`** — the bunker integration and per-device token plumbing must land first. Until then, S2 work in this repo can't move. The S2 effort in satmachineadmin shrinks correspondingly: read `operator_pubkey` off `Payment.extra` and join against the machine row. The hard work moves into the lnbits bunker client. ## Reference - `aiolabs/lnbits#9` — operator-IdP framing (reframed 2026-05-25). - `aiolabs/lnbits#18` — sidecar bunker integration; §F is the satmachineadmin alignment. - NIP-46 spec: `~/dev/nostr-protocol/nips/46.md`. - Design doc: `docs/security-pathway-v1.md` §5.1, §6.S2 — needs a follow-up edit reflecting this pivot. - Previous framing (NIP-26): preserved here in git history for context; not the path forward.
padreug changed title from S2 — NIP-26 delegation enforcement in nostr-transport handler to S2 — NIP-46 connection-token enforcement on kind-21000 RPC (was: NIP-26 delegation) 2026-05-25 23:00:36 +00:00
Author
Owner

➡️ Migrated to aiolabs/spirekeeper#10 (aiolabs/spirekeeper#10).

The v2-bitspire line of this extension now lives in its own repo, aiolabs/spirekeeper. Tracking for this issue continues there; closing here. (Issue numbers were reassigned in the new repo.)

➡️ **Migrated to https://git.atitlan.io/aiolabs/spirekeeper/issues/10 (aiolabs/spirekeeper#10).** The v2-bitspire line of this extension now lives in its own repo, `aiolabs/spirekeeper`. Tracking for this issue continues there; closing here. (Issue numbers were reassigned in the new repo.)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/satmachineadmin#16
No description provided.