S0 — Seed-URL pairing + ATM keypair separation #9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #8. 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 #8 status block and lnbits#9). The seed URL now carries a NIP-46
bunker://connection URL issued by the sidecarnsecbunkerd(lnbits#18 §F).Sequencing decision (per epic #8): 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:99inaiolabs/lamassu-nextwrites the operator's ownnsecinto/var/lib/bitspire/.envasVITE_ATM_PRIVATE_KEY. Physical compromise of the ATM ⇒ total operator compromise on every relay.Proposed (post-lnbits#18)
nsecand never overwrites it with the operator's. (It already generates one whenATM_PRIVATE_KEYis unset — we just stop the manual override.)bunker://<target_pubkey>?relay=<url>&secret=<token>URL.{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).bunker_admin.revoke_user(target_pubkey)→ bunker rejects subsequent requests. Re-pair issues a fresh token.Changes
aiolabs/satmachineadminPOST /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).dca_machinesgetsbunker_connection_id+bunker_token_expires_atcolumns so we can show the expiry + a "Re-pair" button.aiolabs/lamassu-nextdeploy/nixos/provision-atm.sh: stop acceptingATM_PRIVATE_KEY=<operator nsec>; instead consume a seed URL via--seed-urlflag or stdin.Acceptance
/var/lib/bitspire/.envcontains only the ATM's own nsec + a bunker connection URL — never the operator's nsec.Reference
aiolabs/lnbits#18§F — per-device scoped tokens (this issue's authority primitive).~/dev/nostr-protocol/nips/46.md.docs/security-pathway-v1.md§5.1, §6.S0 — needs a follow-up edit reflecting the NIP-26→NIP-46 swap.Status 2026-06-16 — unblocked; API correction; naming; gate-reset acceptance
Unblocked. The lnbits-side primitive this issue waited on shipped on
lnbitsdev(lnbits#18 status 2026-06-16):NsecBunkerAdminClient.create_new_token+ policies, verified against the real bunker.API correction. The pseudocode here (
create_token(target_pubkey, perms=["sign_event:21000"], expires_at=...)) does NOT match the shipped admin client. Actual flow is policy-based:The seed URL embeds that
bunker_url.Naming. Producer is now
aiolabs/spirekeeper(v2-bitspire split from satmachineadmin 2026-06-13). Read every "satmachineadmin" here as "spirekeeper". ATM-side counterpart = bitspire#52; cassette state = bitspire#56.New acceptance criterion — folds in the cassette-bootstrap gap. The ATM publishes its
bitspire-cassettes-statehello-event exactly once, gated bystate.db meta.bootstrapPublishedAt(bitspire#56). Re-pointing an ATM at a new operator/relay today never re-publishes it (reproduced on demo 2026-06-16 — cassettes never populate after re-pairing). The pairing consumer MUST, on consuming a new/changed seed:bootstrapPublishedAt(→ re-publish cassette-state to the new operator), andThis makes the standalone gate bug a non-issue rather than a separate patch.
Correction 2026-06-16 — ATM self-signs (path B), not sign-as-operator; model = per-ATM bunker key (A1)
The original pseudocode has the ATM signing kind-21000 as the operator (
target_pubkey=operator_pubkey,bunker://<operator_pubkey>). That contradicts the shipped path-B roster (nostr_transport/roster.py, live on demo), which routes by the kind-21000 sender pubkey = the ATM's own npub. Signing as the operator would make the sender the operator and the roster lookup never fire.Resolved model (confirmed 2026-06-16): the ATM self-signs with its own key; the roster maps that npub -> operator wallet. The bunker's role for the ATM is to hold the ATM's own key (no nsec on the ATM disk), NOT to let the ATM sign as the operator.
Pairing flow (model A1 — chosen 2026-06-16):
bunker.create_new_key(name=f"atm-{machine.id}", ...)-> the ATM's own npub, held in the bunker.create_new_token(key_name=f"atm-{machine.id}", client_name=..., policy_id)-> recoverbunker://viaget_key_tokens.dca_machines.machine_npub= the bunker-generated ATM npub (so roster routes it). Seed URL embeds{atm_npub, bunker_url, relay_list}.Lifecycle consequence:
machine_npubis assigned at pair time (bunker-generated), not entered by the operator at create time.