334 lines
12 KiB
Markdown
334 lines
12 KiB
Markdown
# 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<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 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 <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]
|
||
```
|