Files
n_signer/documents/nsigner_menus.md
T

334 lines
12 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.
# 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 14 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 112, 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 18, 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 18, 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<version> — 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=<locked|unlocked> (<N> words) signer=<name> derived=<count>`
**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<version> — 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:<hex>`)
- `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 14 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 <mode>` | Skips transport selection menu (menu 3) |
| `--mnemonic-stdin` | Skips mnemonic menu (menu 1), reads from stdin |
| `--mnemonic-fd <N>` | Skips mnemonic menu (menu 1), reads from fd N |
| `--allow-all` | Skips approval prompts (sets `prompt_always_allow`) |
| `--socket-name <name>` | Sets the socket name (skips random name generation) |
| `--preapprove <SPEC>` | 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]
```