Files

12 KiB
Raw Permalink Blame History

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
  • nrequires_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

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]