# nsigner Menu Reference This document describes every interactive menu and screen in the `nsigner` TUI, in the order they appear during a session. Use this as the authoritative reference when discussing changes to the user experience. ## Startup sequence The menus appear in this order during interactive (TUI) startup: 1. **Unlock — Mnemonic source** 2. **Define a role — Role preset menu** (loops) 3. **Transport — Transport selection** 4. **Running phase — Main status display** Non-interactive startup (`--mnemonic-stdin`, `--mnemonic-fd`, or piped input) skips menus 1–4 and creates a default `main` role automatically. --- ## 1. Unlock — Mnemonic source **When:** First screen, before anything else. **Title:** `> Unlock` **Prompt:** ``` Mnemonic source: [E]nter existing or [G]enerate new Default is E; you can also paste full mnemonic here. > ``` **Options:** | Input | Action | |-------|--------| | `E` (default) | Prompt for an existing mnemonic (echo disabled) | | `G` | Generate a fresh 12-word BIP-39 mnemonic from `getrandom(2)`, display it numbered with a "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN" warning | | Paste full mnemonic | If the input contains spaces and doesn't start with `G`, it's treated as a mnemonic and validated directly | | `q` / `x` | Exit | **After `E`:** Prompts for the mnemonic phrase with terminal echo disabled. Validates BIP-39 checksum. Up to 10 invalid attempts before exit. **After `G`:** Displays the generated mnemonic numbered 1–12, then continues. --- ## 2. Define a role — Role preset menu **When:** After mnemonic is loaded, in TUI mode only. **Title:** `Define a role — bind a role name to a derivation path template` **Prompt:** ``` Define a role: 1. Standard Nostr (NIP-06): secp256k1, m/44'/1237'/0'/0/0 2. Standard Nostr range: secp256k1, m/44'/1237'/*'/0/0 3. Nostr agent range (hardened): secp256k1, m/44'/1237'/*'/1'/0' 4. SSH role: ed25519, m/44'/102001'/0'/0'/0' 5. Age/x25519 role: x25519, m/44'/102002'/0'/0'/0' 6. ML-DSA-65 role: post-quantum signatures, m/44'/102003'/0'/0'/0' 7. SLH-DSA-128s role: post-quantum signatures, m/44'/102004'/0'/0'/0' 8. ML-KEM-768 role: post-quantum KEM, m/44'/102005'/0'/0'/0' 9. OTP role (one-time pad encryption) 10. Custom path Select [1]: ``` **Preset defaults:** | Choice | Default name | Default path | Curve | Purpose | |--------|-------------|-------------|-------|---------| | 1 | `main` | `m/44'/1237'/0'/0/0` | secp256k1 | nostr | | 2 | `nostr_range` | `m/44'/1237'/*'/0/0` | secp256k1 | nostr | | 3 | `nostr_agent` | `m/44'/1237'/*'/1'/0'` | secp256k1 | nostr | | 4 | `ssh` | `m/44'/102001'/0'/0'/0'` | ed25519 | ssh | | 5 | `age` | `m/44'/102002'/0'/0'/0'` | x25519 | age | | 6 | `ml_dsa_65` | `m/44'/102003'/0'/0'/0'` | ml-dsa-65 | pq_sig | | 7 | `slh_dsa_128s` | `m/44'/102004'/0'/0'/0'` | slh-dsa-128s | pq_sig | | 8 | `ml_kem_768` | `m/44'/102005'/0'/0'/0'` | ml-kem-768 | pq_kem | | 9 | `otp` | (pad file) | otp | n/a | | 10 | `custom` | `m/44'/1237'/0'/0/0` | (prompted) | (auto-detected) | **After selecting a preset, the user is prompted for:** ### 2a. Role name ``` Role name [main]: ``` Editable line (arrow keys, backspace). Defaults to the preset's default name. If the name already exists, it's skipped. ### 2b. Curve (only for choice 10 — Custom) ``` Curve: 1) secp256k1 (Nostr, Bitcoin) 2) ed25519 (SSH) 3) x25519 (key agreement, Age) 4) ml-dsa-65 (post-quantum signatures) 5) slh-dsa-128s (post-quantum signatures) 6) ml-kem-768 (post-quantum KEM) Select [1]: ``` For presets 1–8, the curve is set automatically. For OTP (9), no curve is needed. ### 2c. Path template (only for choice 10 — Custom) ``` Path template [m/44'/1237'/0'/0/0]: ``` Editable line. Pre-filled with the default path (updated to match the selected curve). Supports range syntax (`0-1000'`), set syntax (`1+34+54`), and wildcard (`*'` for any index). For presets 1–8, the path is set automatically from the preset — no prompt. For OTP (9), no path is needed. ### 2c-otp. OTP pad file (only for choice 9 — OTP) ``` OTP pad directory (e.g. /media/usb0): OTP pad name (e.g. mypad): ``` Prompts for the pad directory and pad name. The pad is bound immediately. If binding fails, the role is skipped. ### 2d. Requires interactive approval ``` Require interactive approval for each request? [Y/n]: ``` - `Y` (default) → `requires_approval = 1` — human attendant must approve each request - `n` → `requires_approval = 0` — role name is the password, no prompt (role-as-password) ### 2e. Confirmation ``` Role 'main' registered: curve=secp256k1 path=m/44'/1237'/0'/0/0 (fixed, requires_approval=1). ``` Or for templated paths: ``` Role 'nostr_agent' registered: curve=secp256k1 path=m/44'/1237'/%d'/1'/0' (range 0-1000, requires_approval=1). ``` ### 2f. Loop ``` Define another role? [y/N]: ``` - `y` → back to the preset menu - `N` (default) → continue to transport selection **Mandatory:** At least one role must be created. If the user exits without creating any roles, the signer prints "At least one role must be defined." and exits. --- ## 3. Transport — Transport selection **When:** After role definition, in TUI mode with no `--listen` flag. **Title:** `Transport — how should other programs reach this signer?` **Prompt:** ``` Select one or more (type a number to toggle, 'a' for all, Enter to confirm): [x] 1. Local Unix socket (same machine/qube) [ ] 2. Qubes qrexec bridge (other qubes via qrexec, no network) [ ] 3. FIPS/TCP listener (framed JSON, FIPS mesh or local network) [ ] 4. HTTP listener (curl-friendly, localhost by default) [a] select all Enter = confirm > ``` **Options:** | Input | Action | |-------|--------| | `1` | Toggle Local Unix socket | | `2` | Toggle Qubes qrexec bridge | | `3` | Toggle FIPS/TCP listener | | `4` | Toggle HTTP listener | | `a` | Select all | | Enter | Confirm current selection (at least one required) | Default: Unix socket only. --- ## 4. Running phase — Main status display **When:** After all startup menus, this is the main screen. **Title:** `> Main Menu` **Layout:** ``` ┌ n_signer v — Main Menu ─────────────────────────┐ │ │ │ Roles: │ │ Role Purpose Curve Derivation path │ -------------------- ------------ ------------ ------------------------ │ main nostr secp256k1 m/44'/1237'/0'/0/0 │ role1 nostr secp256k1 m/44'/1237'/1-100'/0/0 │ │ │ Activity (latest first): │ │ 14:51:05 uid:1000 nostr_get_public_key(pathrole_2) ALLOWED:prompt │ 14:50:54 uid:1000 nostr_get_public_key(pathrole_2) ALLOWED:prompt │ │ │ session=unlocked (12 words) signer=nsigner derived=2 │ │ │ │ l lock/reunlock r refresh d display connections q/x quit │ │ └───────────────────────────────────────────────────────────┘ > ``` **Status line:** `session= ( words) signer= derived=` **Menu items:** | Key | Action | |-----|--------| | `l` | Lock / re-unlock (re-prompt for mnemonic) | | `r` | Refresh display | | `d` | Display connections (show transport details + example client commands) | | `q` / `x` | Quit | **Note:** The `a toggle auto-approve` menu item has been **removed**. Authorization is now per-role via the `requires_approval` flag set during role definition. --- ## 5. Approval prompt **When:** A client request arrives for a role with `requires_approval = 1`, and the request is not pre-approved by policy. **Title:** `> Approval` **Layout:** ``` ┌ n_signer v — Approval ──────────────────────────┐ │ │ │ Approval required │ │ caller: uid:1000 │ │ method: nostr_sign_event │ │ role: main │ │ purpose: nostr │ │ ** NEW IDENTITY — will be derived if approved ** │ │ │ │ y: allow once │ │ n: deny │ │ e: allow this caller+role+verb for session │ │ a: allow this caller+role for session (all verbs) │ │ │ └───────────────────────────────────────────────────────────┘ > ``` **Options:** | Key | Action | Policy result | |-----|--------|---------------| | `y` | Allow this one request | `POLICY_ALLOW` | | `n` | Deny this request | `POLICY_DENY` | | `e` | Allow this caller+role+verb for the rest of the session | `POLICY_ALLOW_SESSION_VERB` | | `a` | Allow this caller+role for all verbs for the session | `POLICY_ALLOW_SESSION_ALL` | The `** NEW IDENTITY — will be derived if approved **` line appears only when the requested key hasn't been derived yet. **Fields shown:** - `caller` — the caller identity (e.g. `uid:1000`, `qubes:vm-name`, `pubkey:`) - `fips peer` — (TCP/FIPS mode only) the peer's npub and optional name - `method` — the JSON-RPC verb (e.g. `nostr_sign_event`, `nostr_get_public_key`) - `role` — the role name from the request - `purpose` — the role's purpose (nostr, ssh, age, pq_sig, pq_kem) --- ## 6. Display connections **When:** Pressed `d` from the main status display. **Shows:** For each active transport, a section with: - Transport name and description - Socket address / port / URL - Example client command After any keypress, returns to the main status display. --- ## Non-interactive mode When started with `--mnemonic-stdin` or `--mnemonic-fd`, or when stdin is not a TTY: - Menus 1–4 are skipped - A default `main` role is created automatically: `secp256k1`, `m/44'/1237'/0'/0/0`, `requires_approval=1` - The `--allow-all` flag sets `server_set_prompt_always_allow(1)` which bypasses approval prompts (used by tests and automated setups) - The `NSIGNER_TEST_NONINTERACTIVE_PROMPT` env var can be set to `allow` or `deny` to control the non-interactive prompt fallback --- ## CLI flags that affect menus | Flag | Effect | |------|--------| | `--listen ` | Skips transport selection menu (menu 3) | | `--mnemonic-stdin` | Skips mnemonic menu (menu 1), reads from stdin | | `--mnemonic-fd ` | Skips mnemonic menu (menu 1), reads from fd N | | `--allow-all` | Skips approval prompts (sets `prompt_always_allow`) | | `--socket-name ` | Sets the socket name (skips random name generation) | | `--preapprove ` | Pre-approves specific caller+role+verb combinations | --- ## Summary flow ```mermaid flowchart TD A[Start] --> B{stdin is TTY?} B -- No --> C[Non-interactive: create default main role] B -- Yes --> D[Menu 1: Unlock — mnemonic source] D --> E[Menu 2: Define a role — preset menu] E --> F{Another role?} F -- Yes --> E F -- No --> G{At least one role?} G -- No --> H[Error: at least one role required] G -- Yes --> I{--listen flag?} I -- No --> J[Menu 3: Transport selection] I -- Yes --> K[Use --listen mode] J --> N[Menu 4: Main status display] K --> N C --> N N --> O{Request arrives} O --> P{requires_approval?} P -- No --> Q[Authorize immediately] P -- Yes --> R{Pre-approved?} R -- Yes --> Q R -- No --> S[Menu 6: Approval prompt] S -- y/e/a --> Q S -- n --> T[Deny] ```