S0 — Seed-URL pairing + ATM keypair separation #14

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

Part of #13. Closes gaps G3 (ATM holds operator nsec) and G9 (no ACL on auto-account-from-npub).

2026-05-26 — payload swap (NIP-26 → NIP-46 bunker URL) + blocked

Original framing had the seed URL carry a NIP-26 delegation token signed by the operator's nsec. That primitive is dead (see epic #13 status block and lnbits#9). The seed URL now carries a NIP-46 bunker:// connection URL issued by the sidecar nsecbunkerd (lnbits#18 §F).

Sequencing decision (per epic #13): wait for lnbits#18 to land before shipping S0. No transitional shim — wiring a seed URL format we'd rewrite in two weeks is throwaway work. The ATM keeps the Option 1 stopgap (operator nsec on disk) until the bunker is real and S0 ships alongside S2 + S7 in Sprint 2.

Today's stopgap (Option 1, stays in place until Sprint 2)

deploy/nixos/provision-atm.sh:99 in aiolabs/lamassu-next writes the operator's own nsec into /var/lib/bitspire/.env as VITE_ATM_PRIVATE_KEY. Physical compromise of the ATM ⇒ total operator compromise on every relay.

Proposed (post-lnbits#18)

  1. ATM has its own keypair. Provisioning script generates a fresh nsec and never overwrites it with the operator's. (It already generates one when ATM_PRIVATE_KEY is unset — we just stop the manual override.)
  2. Operator-side authority comes from a NIP-46 connection token (per lnbits#18 §F):
    token = await bunker_admin.create_token(
        target_pubkey=operator_pubkey,
        client_name=f"satmachine-{machine.id}",
        perms=["sign_event:21000"],
        expires_at=int(time.time()) + 86400 * 30,
    )
    
    Returns a bunker://<target_pubkey>?relay=<url>&secret=<token> URL.
  3. One-shot seed URL containing {atm_npub, bunker_url, relay_list} is rendered as QR on the operator's satmachineadmin dashboard and consumed by the ATM at first boot (or pasted as a string via web UI).
  4. Revocation = operator clicks "Revoke ATM access" in the dashboard → satmachineadmin calls bunker_admin.revoke_user(target_pubkey) → bunker rejects subsequent requests. Re-pair issues a fresh token.

Changes

aiolabs/satmachineadmin

  • New API: POST /api/v1/dca/machines/:id/pair → returns one-shot seed URL with a freshly-issued bunker connection URL (uses the lnbits admin client introduced by lnbits#18).
  • New UI on the Fleet tab: "Pair ATM" wizard with QR + copy-to-clipboard fallback.
  • dca_machines gets bunker_connection_id + bunker_token_expires_at columns so we can show the expiry + a "Re-pair" button.

aiolabs/lamassu-next

  • deploy/nixos/provision-atm.sh: stop accepting ATM_PRIVATE_KEY=<operator nsec>; instead consume a seed URL via --seed-url flag or stdin.
  • ATM-side loader (lamassu-next app) reads bunker URL from sealed config and uses it for NIP-46 signing.

Acceptance

  • Operator pairs a fresh ATM via the wizard end-to-end on the regtest stack.
  • /var/lib/bitspire/.env contains only the ATM's own nsec + a bunker connection URL — never the operator's nsec.
  • After "Revoke ATM access" → the ATM's next kind-21000 is rejected by the handler.
  • Revocation visible in operator dashboard.

Reference

  • aiolabs/lnbits#18 §F — per-device scoped tokens (this issue's authority primitive).
  • NIP-46 spec: ~/dev/nostr-protocol/nips/46.md.
  • Design doc: docs/security-pathway-v1.md §5.1, §6.S0 — needs a follow-up edit reflecting the NIP-26→NIP-46 swap.
  • Predecessor sketch: #12.
Part of #13. Closes gaps G3 (ATM holds operator nsec) and G9 (no ACL on auto-account-from-npub). ## 2026-05-26 — payload swap (NIP-26 → NIP-46 bunker URL) + blocked Original framing had the seed URL carry a NIP-26 delegation token signed by the operator's nsec. That primitive is dead (see epic #13 status block and lnbits#9). The seed URL now carries a **NIP-46 `bunker://` connection URL** issued by the sidecar `nsecbunkerd` (lnbits#18 §F). **Sequencing decision (per epic #13): wait for lnbits#18 to land before shipping S0.** No transitional shim — wiring a seed URL format we'd rewrite in two weeks is throwaway work. The ATM keeps the Option 1 stopgap (operator nsec on disk) until the bunker is real and S0 ships alongside S2 + S7 in Sprint 2. ## Today's stopgap (Option 1, stays in place until Sprint 2) `deploy/nixos/provision-atm.sh:99` in `aiolabs/lamassu-next` writes the operator's own `nsec` into `/var/lib/bitspire/.env` as `VITE_ATM_PRIVATE_KEY`. Physical compromise of the ATM ⇒ total operator compromise on every relay. ## Proposed (post-lnbits#18) 1. **ATM has its own keypair.** Provisioning script generates a fresh `nsec` and never overwrites it with the operator's. (It already generates one when `ATM_PRIVATE_KEY` is unset — we just stop the manual override.) 2. **Operator-side authority** comes from a NIP-46 connection token (per lnbits#18 §F): ```python token = await bunker_admin.create_token( target_pubkey=operator_pubkey, client_name=f"satmachine-{machine.id}", perms=["sign_event:21000"], expires_at=int(time.time()) + 86400 * 30, ) ``` Returns a `bunker://<target_pubkey>?relay=<url>&secret=<token>` URL. 3. **One-shot seed URL** containing `{atm_npub, bunker_url, relay_list}` is rendered as QR on the operator's satmachineadmin dashboard and consumed by the ATM at first boot (or pasted as a string via web UI). 4. **Revocation** = operator clicks "Revoke ATM access" in the dashboard → satmachineadmin calls `bunker_admin.revoke_user(target_pubkey)` → bunker rejects subsequent requests. Re-pair issues a fresh token. ## Changes **`aiolabs/satmachineadmin`** - New API: `POST /api/v1/dca/machines/:id/pair` → returns one-shot seed URL with a freshly-issued bunker connection URL (uses the lnbits admin client introduced by lnbits#18). - New UI on the Fleet tab: "Pair ATM" wizard with QR + copy-to-clipboard fallback. - `dca_machines` gets `bunker_connection_id` + `bunker_token_expires_at` columns so we can show the expiry + a "Re-pair" button. **`aiolabs/lamassu-next`** - `deploy/nixos/provision-atm.sh`: stop accepting `ATM_PRIVATE_KEY=<operator nsec>`; instead consume a seed URL via `--seed-url` flag or stdin. - ATM-side loader (lamassu-next app) reads bunker URL from sealed config and uses it for NIP-46 signing. ## Acceptance - [ ] Operator pairs a fresh ATM via the wizard end-to-end on the regtest stack. - [ ] `/var/lib/bitspire/.env` contains only the ATM's own nsec + a bunker connection URL — never the operator's nsec. - [ ] After "Revoke ATM access" → the ATM's next kind-21000 is rejected by the handler. - [ ] Revocation visible in operator dashboard. ## Reference - `aiolabs/lnbits#18` §F — per-device scoped tokens (this issue's authority primitive). - NIP-46 spec: `~/dev/nostr-protocol/nips/46.md`. - Design doc: `docs/security-pathway-v1.md` §5.1, §6.S0 — needs a follow-up edit reflecting the NIP-26→NIP-46 swap. - Predecessor sketch: #12.
padreug changed title from S0 — Seed-URL pairing + ATM keypair separation (NIP-26 delegation) to S0 — Seed-URL pairing + ATM keypair separation 2026-05-25 23:02:32 +00:00
Author
Owner

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

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/9 (aiolabs/spirekeeper#9).** 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#14
No description provided.