Files
archy/docs/nostr-signer-login-research.md
T
archipelagoandClaude Fable 5 9e264611e2 docs: Nostr signer-login research + identity-import UX plan
Research report on the most frictionless "sign in with your Nostr signer"
flow for node login (NIP-07 extension + NIP-46 QR scan with Amber, password
always kept as fallback), and a plan for adding existing Nostr identities
(nsec import / npub watch-only / browser extension) to the Nostr Identities
screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:59:54 -04:00

96 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sign in to the node with a Nostr signer — research & recommendation
**Status:** research only (2026-07-16), no code. Companion plan: `docs/nostr-identity-import-plan.md`.
## What's already in the tree (and what it isn't)
The IndeeHub "sign in with signer" work is the *inverse* of this feature: the node acts
as a NIP-07 **provider** for embedded iframe apps, signing with node-held keys
(`useNostrBridge.ts` postMessage bridge → `identity.nostr-sign` etc., picker UI in
`NostrIdentityPicker.vue`). It never verifies an external signer — but the UI patterns
(picker modal, QR rendering) and the backend crypto are reusable:
- **`nostr-sdk 0.44` is already a core dependency** (`nostr_handshake.rs` runs a real
relay client) — schnorr event verification and NIP-46 client support are essentially
free on the Rust side.
- Auth today is single-password + optional TOTP, and TOTP already uses a **two-step
login** (`auth.login``auth.login.totp`) — the exact slot where a parallel
`auth.login.nostr.*` path fits.
- The node can host its own relay (strfry app), and the frontend already bundles `qrcode`.
## Candidate flows, ranked by friction
### A. Browser extension (NIP-07) — lowest friction on desktop (2 clicks)
Login page shows "Sign in with extension" when `window.nostr` exists. Server issues a
random challenge → extension signs a **kind 22242** auth event carrying the challenge →
server verifies signature + challenge + `created_at` freshness + that the pubkey is
enrolled → normal session cookie. ~50 lines of frontend, ~80 lines of Rust. No relay
involved at all.
### B. QR scan with a mobile signer (NIP-46 `nostrconnect://`) — the headline UX (scan + 1 tap)
1. Backend generates an ephemeral client keypair and renders a
`nostrconnect://<pubkey>?relay=<url>&secret=<rand>&perms=sign_event:22242&name=Archipelago` QR.
2. User scans with **Amber** (Android reference signer; Aegis/Nowser also scan;
nsec.app is paste-based; Alby is *not* a NIP-46 signer).
3. Phone connects to the relay, acks the secret; backend requests one
`sign_event:22242` over the encrypted NIP-46 channel, verifies, issues the session.
**Key architectural choice:** make the **Rust backend the NIP-46 client** (rust-nostr's
`nostr-connect` crate), talking to the relay over localhost — the browser only polls our
own RPC for "signer connected". No websocket/mixed-content issues in the Vue app.
**Relay topology:** no public relay is required by the spec — and public relays often
rate-limit ephemeral NIP-46 traffic. The node's own strfry is the ideal relay (private,
LAN-fast); the QR should carry a relay URL derived from the Host the browser used
(LAN IP / Tailscale IP — not `.local`, which Android often can't resolve).
**One empirical blocker to test first: does Amber accept plain `ws://` LAN relays?**
(Self-signed `wss://` will likely fail cert validation.) If not, route `wss://` through
the existing nginx/HTTPS cert story.
### C. Remembered NIP-46 session (persisted bunker pointer) — zero-tap repeat logins
Same as B but persists the pairing so future logins auto-approve. Adds state,
revocation surface, and "bunker offline = silent hang" failure modes. **Defer** — B
re-scans in ~5 seconds anyway.
## Recommendation
Ship **A + B behind one "Sign in with Nostr" button**; skip C for now. Password (+TOTP)
stays the permanent fallback — exactly as the user proposed, the signer is enrolled in a
step *after* password creation, never instead of it. The verification core is one shared
Rust function (sig + challenge + freshness + enrolled-pubkey → session).
- **Onboarding:** after the password (and seed) steps, an optional "Connect a signer"
card: QR (nostrconnect) + "Use browser extension" + Skip. Success enrolls the npub as
a login key.
- **Settings (next to TOTP):** list enrolled npubs (added date + method), "Add npub"
(paste, becomes usable after a challenge-verify), "Connect another signer" (same
QR/extension modal), "Remove" (requires password confirm; removing the last npub never
locks the account — password always works).
- **Libraries:** hand-roll the 22242 event for NIP-07 (window.nostr is a browser global);
rust-nostr `nostr-connect` for NIP-46. Avoid the 2.4 MB `nostr-login` JS bundle —
wrong fit for a self-hosted box (defaults to public bunkers); it's UX prior art only.
## Security notes
- Only pubkeys enrolled **while authenticated** (or during onboarding) may log in —
a simple `login_npubs` list next to the TOTP data in `auth.rs`.
- Challenge: 32-byte random, single-use, 25 min TTL, `created_at` ±60 s, deleted on
first verify attempt; pin an origin/host tag. Rate-limit like password attempts.
- The `secret` in the nostrconnect URI is a bearer token — one QR per attempt, expires
with the challenge.
- Policy call: signer approval should count as the second factor for TOTP accounts
(possession of phone/extension key), so nostr login doesn't silently bypass TOTP.
## Open questions
1. Amber + `ws://` LAN relay — needs a 10-minute on-device test before committing.
2. Which relay URL to embed (LAN vs Tailscale vs onion) — derive from browser Host.
3. NIP-46 encryption: spec says NIP-44, some signers still NIP-04 — rust-nostr handles
both; verify against current Amber.
4. Track draft **NIP-97 "Login with Nostr"** (matches this UX exactly, unmerged) —
align, don't depend.
**Prior art:** no mainstream self-hosted node OS (Umbrel, Start9, Alby Hub) ships Nostr
QR login for its own UI — this would be genuinely differentiating, and every building
block is already in the tree.