11 KiB
11 KiB
Menu Gap Analysis: C main.c vs Rust signer
Source of truth: the C code in src/main.c, NOT
documents/nsigner_menus.md (which is
stale — e.g. it claims the wizard prompts Require interactive approval? [Y/n], but
the actual C code hardcodes requires_approval = 0 and never prompts).
This document compares every interactive menu/screen in the C implementation
against the current Rust port in src/tui.rs and
src/main.rs.
Legend: ✅ matches, ⚠️ partial, ❌ missing/divergent.
Menu 1 — Unlock / Mnemonic source
- Frame:
n_signer v<ver> > Unlock, content screen title"Load mnemonic" - Prompt:
Mnemonic source: [E]nter existing or [G]enerate new Default is E; you can also paste full mnemonic here.q/Q/x/X(single char) → exit with error- Paste detection: if input contains a space and doesn't start with
g/G, treat as mnemonic and validate directly g/G→ generate 12-word, print numbered"%2d. %s", warningGenerated mnemonic (WRITE THIS DOWN - it will not be shown again):, thenPress Enter after writing down your mnemonic to continue.- Otherwise → second screen
> Unlock/"Enter mnemonic",Enter mnemonic (12/15/18/21/24 words):,q/xto exit - 10 invalid attempts then
Too many invalid mnemonic attempts (10). Exiting. - Success:
Seed phrase is valid and accepted.
Rust (load_mnemonic_tui):
- Frame:
nsigner v<ver> > Unlock, title"Enter mnemonic phrase" - Prompt:
Enter your BIP-39 mnemonic phrase, or 'g' to generate a new one. g/G→ generate, numbered, warning ✅- Otherwise → load as mnemonic (paste works implicitly) ⚠️
- No
q/xexit ❌ - No 10-attempt limit ❌
- No second "Enter mnemonic" screen ❌
- No explicit paste-detection branch (works by accident) ⚠️
| Feature | C | Rust | Status |
|---|---|---|---|
[E]/[G] prompt text |
yes | different wording | ⚠️ |
q/x to exit |
yes | no | ❌ |
| Paste detection | yes | implicit | ⚠️ |
| 10-attempt limit | yes | no | ❌ |
| Generate + warning | yes | yes | ✅ |
| Second "Enter mnemonic" screen | yes | no | ❌ |
Menu 2 — Define a role / Role preset menu
for(;;)loop, content screen title"Define a role — bind a role name to a derivation path template"- 10 presets (1–10): 1=Standard Nostr, 2=Nostr range, 3=Nostr agent, 4=SSH, 5=Age, 6=ML-DSA-65, 7=SLH-DSA-128s, 8=ML-KEM-768, 9=OTP role, 10=Custom path
Select [1]:(default 1 if empty)- Role name prompt:
Role name [%s]:with editable line + default - Duplicate →
Role '%s' already exists, skipping.+ continue - Choice 9 (OTP): prompts
OTP pad directory (e.g. /media/usb0):andOTP pad name (e.g. mypad):, binds pad immediately, registers role withcurve_str="otp",requires_approval=0 - Choice 10 (Custom): curve menu
then
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]:Path template [%s]:editable requires_approvalis HARDCODED to 0 — no prompt (doc is wrong)- Confirmation:
Role '%s' registered: curve=%s path=%s (fixed, requires_approval=0).or(range %d-%d, requires_approval=0). - Loop:
Define another role? [y/N]:→ycontinues, else break - Mandatory:
if (roles_created == 0) { "At least one role must be defined." return -1; } - No auto-register of default
main— user must pick preset 1
Rust (role_wizard):
- Auto-registers default
mainfirst before showing menu ❌ - 9 presets (1–9) with 9=Custom, plus
0=Done ❌ (no OTP preset) - No OTP pad prompts ❌
- Custom: prompts path only, curve auto-detected from path (no curve menu) ⚠️
- No
requires_approvalprompt (matches C's hardcoded 0) ✅ - No confirmation line ❌
- Loop via
0/Done instead ofDefine another role? [y/N]⚠️ - Mandatory ≥1 role satisfied by auto-register (divergent mechanism) ⚠️
| Feature | C | Rust | Status |
|---|---|---|---|
| 10 presets (incl. OTP) | yes | 9, no OTP | ❌ |
| Auto-register default main | no | yes | ❌ |
| Curve menu (custom) | yes | auto-detect | ⚠️ |
| OTP pad dir/name prompts | yes | no | ❌ |
| requires_approval prompt | no (hardcoded 0) | no | ✅ |
| Confirmation line | yes | no | ❌ |
Loop y/N |
yes | 0/Done |
⚠️ |
| Mandatory ≥1 role | yes | yes (via auto-register) | ⚠️ |
Menu 3 — Transport selection
C (prompt_transport_selection):
- Content screen title
"Transport — how should other programs reach this signer?" Select one or more (type a number to toggle, 'a' for all, Enter to confirm):- Multi-toggle checkboxes
[x]/[ ]:- Local Unix socket
- Qubes qrexec bridge
- FIPS/TCP listener
- HTTP listener
[a] select all Enter = confirm1-4toggles bits,aselects all, Enter confirms (≥1 required)- Default: Unix socket only
Rust (transport_selection):
- Title
"Transport selection" - Single-select (pick one of 4): Unix, TCP, HTTP, Unix+HTTP
- No Qubes qrexec ❌
- No toggle/checkbox UI ❌
- No
afor all ❌
| Feature | C | Rust | Status |
|---|---|---|---|
| Multi-select toggle | yes | no (single) | ❌ |
| Qubes qrexec option | yes | no | ❌ |
a select all |
yes | no | ❌ |
| Enter to confirm | yes | no | ❌ |
| Default Unix | yes | yes | ✅ |
Menu 4 — Main status display
C (render_status, g_main_menu_items):
- Frame
n_signer v<ver> > Main Menu ^*Roles^:+ table (Role/Purpose/Curve/Derivation path) or(none)^*Activity (latest first)^:+ log entries or(none)- Status line:
session=<locked|unlocked> (<N> words) signer=<name> derived=<count> - Menu:
^_l^: lock/reunlock,^_r^: refresh,^_d^: display connections,^_q^:/x quit
Rust (render_status, MAIN_MENU_ITEMS):
| Feature | C | Rust | Status |
|---|---|---|---|
| Top frame + breadcrumb | yes | yes | ✅ |
| Roles table (4 cols) | yes | yes | ✅ |
| Activity log | yes | yes | ✅ |
| Status line | yes | yes | ✅ |
| Menu items l/r/d/q | yes | yes | ✅ |
Menu 5 — Approval prompt
C (tui_approval_cb):
- Frame
n_signer v<ver> > Approval, title"Approval required" caller: <id>fips peer: <npub> (<name>)ifreq->fips_peer_npubpresentmethod:,role:,purpose:** NEW IDENTITY — will be derived if approved **ifpending_derivation^_y^: allow once,^_n^: deny,^_e^: allow this caller+role+verb for session,^_a^: allow this caller+role for session (all verbs)- Reads first char,
a/e/y→ respective policy, else DENY
Rust (approval_prompt):
| Feature | C | Rust | Status |
|---|---|---|---|
| caller/method/role/purpose | yes | yes | ✅ |
| fips peer field | yes | no | ❌ |
| NEW IDENTITY line | yes | no | ❌ |
| y/n/e/a options | yes | yes | ✅ |
Menu 6 — Display connections
C (render_connections):
- Full screen clear, top frame
- Iterates
connection_infoentries built from actual active transports (main.c:4086-4190): Unix, FIPS/TCP, HTTP, Qrexec, Stdio - Each block:
^*<title>^:, connection string,Example:+ example, optional extra - OTP pad status line if bound
- Status line +
Press any key to return
Rust (render_connections):
- Hardcoded Unix + HTTP blocks regardless of active transports ❌
- No Qubes qrexec, no FIPS/TCP, no Stdio blocks ❌
- No OTP pad status line ❌
- "Press any key to return" ✅
| Feature | C | Rust | Status |
|---|---|---|---|
| Reflects active transports | yes | hardcoded | ❌ |
| Qubes qrexec block | yes | no | ❌ |
| FIPS/TCP block | yes | no | ❌ |
| OTP pad status line | yes | no | ❌ |
| Example client commands | yes | partial | ⚠️ |
Summary of divergences (from code, not doc)
flowchart TD
M1[Menu 1 Unlock] --> D1[No q/x exit, no 10-try limit, no second screen]
M2[Menu 2 Role wizard] --> D2[Auto-registers main, no OTP preset, no curve menu, no confirm line]
M3[Menu 3 Transport] --> D3[Single-select not multi-toggle, no Qubes]
M4[Menu 4 Status] --> D4[Matches]
M5[Menu 5 Approval] --> D5[No NEW IDENTITY line, no fips peer]
M6[Menu 6 Connections] --> D6[Hardcoded, not transport-aware, no OTP status]
Highest-impact gaps (behavioral divergence from C code)
- Menu 2 — auto-registers default
mainbefore the wizard. C requires the user to pick preset 1 themselves; the Rust port silently createsmainand then offers to add more. This changes the first-run UX. - Menu 2 — OTP preset (choice 9) missing entirely. Cannot create OTP roles interactively in Rust.
- Menu 2 — no curve menu for Custom (choice 10). C shows a 6-option curve menu; Rust auto-detects from path.
- Menu 3 — Qubes qrexec missing and single-select instead of multi-toggle.
- Menu 1 — no
q/xexit, no 10-attempt limit, no second "Enter mnemonic" screen. - Menu 5 — missing
fips peerfield and** NEW IDENTITY **line. - Menu 6 — hardcoded blocks instead of reflecting actual active transports; no OTP pad status line.
How to spot differences going forward (code-based, not doc-based)
- Treat
src/main.cas the source of truth. The functions to compare against are:prompt_load_mnemonic_tui— Menu 1prompt_named_path_roles— Menu 2prompt_transport_selection— Menu 3render_status— Menu 4tui_approval_cb— Menu 5render_connections— Menu 6
- Keep this file (
plans/menu_gap_analysis.md) as the living checklist; tick rows as the Rust port converges. - Recommended automated check: add an integration test that pipes canned
stdin through each Rust menu and asserts the rendered output contains the
exact prompt strings from the C
printf/tui_printcalls above (e.g.Mnemonic source: [E]nter existing or [G]enerate new,Define a role:,9. OTP role (one-time pad encryption),Select one or more (type a number to toggle, 'a' for all, Enter to confirm):,** NEW IDENTITY — will be derived if approved **). This catches drift mechanically without re-reading the C source each time.