docs: Bolt Card session contract, ADR-003 amendment for verified entry
docs/boltcard-session.md is the /session wire contract (sibling of boltcard-receive-resolver.md), including the trust boundary: the session URL is derived from the card's own host, so open enrollment is still not a security boundary (#91). ADR-003's amendment now records verified entry via /session and the hidden-by-default balance display. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
6779d8ef55
commit
ec2b15c08f
3 changed files with 125 additions and 2 deletions
|
|
@ -10,8 +10,8 @@ The gate landed in aiolabs/bitspire#86 as **Bolt Card tap-to-enter**, not the np
|
|||
|
||||
- **Reader.** The batm3 / upboard reader is a USB CCID contactless reader (Feitian KP382, OMNIKEY 5022) driven by `pcscd` + `nfc-pcsc` in the **main process** (`electron/nfc-service.ts`, from #83), not the serial `/dev/ttyNFC` device or Web NFC. Taps reach the renderer over the existing `nfc:card-tapped` IPC and the store routes them by state (`locked` → enter; the cash screens → pay / receive). The renderer-side `AccessReader` abstraction, the camera npub-QR reader and the mock reader were never wired to anything and have been removed; `services/access/` now holds only `authorize`, the Bolt Card parser and the credential types.
|
||||
- **Credential.** Identity is the card's boltcards `external_id`, parsed **locally** from the tapped `lnurlw` (`AccessScan` kind `boltcard`), not the NFC UID. Only `hashId(external_id, salt)` is compared or logged. The `npub` variant (with its PIN second factor) stays in `authorize()` and its tests for a future non-card credential; `challenge` remains the v2 seam; the planned `uid` variant is gone.
|
||||
- **Soft entry, verify-at-payment.** A tap yields a single-use SUN `p`/`c`. Verifying it at entry would spend the voucher we want to reuse at Complete, so entry makes **no server call**. The stored `lnurlw` is presented once, at Complete, through the #83 / #84 payment paths, and that is where the cryptographic check happens. Consequence: the loaded card is single-shot — after the first Complete attempt, accepted or declined, the store drops it and the customer re-taps.
|
||||
- **Open enrollment is not a security boundary.** With `openEnrollment` on (the current posture on every gated machine), any NDEF tag whose URL contains `/scan/<id>` unlocks the terminal. The gate keeps casual users off the menu; money still only moves on a valid SUN. Closing this — a provisioned allow-list, or a verify-at-entry variant that spends one tap — is tracked in aiolabs/bitspire#91.
|
||||
- **Verified entry via `/session`** (superseding #86's soft entry). A tap yields a single-use SUN `p`/`c`, and verifying it at entry would have spent the voucher Complete needed, so #86 made no server call at entry. The fork now exposes `/session/<id>?p=&c=` (`docs/boltcard-session.md`): it spends the SUN **once**, proves a genuine non-replayed card, returns the card wallet's balance + fiat, and hands back the hit-keyed LUD-03 / LUD-06 second steps so Complete still needs no second tap. The session is single-shot — after the first Complete attempt, accepted or declined, the store drops it and the customer re-taps. The balance is shown on the idle menu and cash screens **hidden by default** behind an eye toggle (`CardChip.vue`), priced the way the LNbits wallet page prices it (wallet currency, else instance default, else the ATM's fiat at its own rate).
|
||||
- **Open enrollment is still not a security boundary.** `/session` proves the card is genuine *to the server the card names*: the session URL is derived from the tapped `lnurlw`'s host, so with `openEnrollment` on a forged NDEF tag pointing at a server that answers `authenticated: true` still unlocks the terminal. Money is unaffected (cash-out never dispenses without `PAYMENT_RECEIVED`; cash-in pays where the holder pointed). Closing the gate means pinning the accepted card-server host(s) and/or a provisioned allow-list — tracked in aiolabs/bitspire#91.
|
||||
- **Session semantics.** One tap = one session; every transaction terminal state returns to `locked`. Inactivity (60 s on the idle menu) and the absolute cap (10 min) are measured at the DOM layer (`useSessionSecurity`) because an XState `after` cannot observe touches. Both send `END_SESSION`, which the machine accepts **from `idle` only**, so a timer can never abandon stacked bills or an in-flight dispense. An explicit End Session button re-locks immediately.
|
||||
- **Config.** `ACCESS_CONTROL_ENABLED` / `ACCESS_OPEN_ENROLLMENT` / `ACCESS_DEV_UNLOCK` / `ACCESS_SALT` from env, overridden by `/var/lib/bitspire/access.json` (pushed with `deploy/nixos/provision-access.sh`, no rebuild). `devUnlock` defaults **off**.
|
||||
- **Audit** is still a console stub; the state.db write promised in decision 7 is tracked in aiolabs/bitspire#90.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue