12 KiB
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:
- Unlock — Mnemonic source
- Define a role — Role preset menu (loops)
- Transport — Transport selection
- 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 requestn→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 menuN(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 namemethod— the JSON-RPC verb (e.g.nostr_sign_event,nostr_get_public_key)role— the role name from the requestpurpose— 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
mainrole is created automatically:secp256k1,m/44'/1237'/0'/0/0,requires_approval=1 - The
--allow-allflag setsserver_set_prompt_always_allow(1)which bypasses approval prompts (used by tests and automated setups) - The
NSIGNER_TEST_NONINTERACTIVE_PROMPTenv var can be set toallowordenyto 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
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]