Files
signer/plans/menu_gap_analysis.md
T

11 KiB
Raw Blame History

Menu Gap Analysis: C main.c vs Rust signer

Source of truth: the C code in src/main.c, NOT documents/signer_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

C (prompt_load_mnemonic_tui):

  • 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", warning Generated mnemonic (WRITE THIS DOWN - it will not be shown again):, then Press Enter after writing down your mnemonic to continue.
  • Otherwise → second screen > Unlock / "Enter mnemonic", Enter mnemonic (12/15/18/21/24 words):, q/x to exit
  • 10 invalid attempts then Too many invalid mnemonic attempts (10). Exiting.
  • Success: Seed phrase is valid and accepted.

Rust (load_mnemonic_tui):

  • Frame: signer 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/x exit ❌
  • 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

C (prompt_named_path_roles):

  • 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): and OTP pad name (e.g. mypad):, binds pad immediately, registers role with curve_str="otp", requires_approval=0
  • Choice 10 (Custom): curve menu
      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]:
    
    then Path template [%s]: editable
  • requires_approval is 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]: → y continues, 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 main first 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_approval prompt (matches C's hardcoded 0) ✅
  • No confirmation line ❌
  • Loop via 0/Done instead of Define 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]/[ ]:
    1. Local Unix socket
    2. Qubes qrexec bridge
    3. FIPS/TCP listener
    4. HTTP listener
  • [a] select all Enter = confirm
  • 1-4 toggles bits, a selects 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 a for 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>) if req->fips_peer_npub present
  • method:, role:, purpose:
  • ** NEW IDENTITY — will be derived if approved ** if pending_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_info entries 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)

  1. Menu 2 — auto-registers default main before the wizard. C requires the user to pick preset 1 themselves; the Rust port silently creates main and then offers to add more. This changes the first-run UX.
  2. Menu 2 — OTP preset (choice 9) missing entirely. Cannot create OTP roles interactively in Rust.
  3. Menu 2 — no curve menu for Custom (choice 10). C shows a 6-option curve menu; Rust auto-detects from path.
  4. Menu 3 — Qubes qrexec missing and single-select instead of multi-toggle.
  5. Menu 1 — no q/x exit, no 10-attempt limit, no second "Enter mnemonic" screen.
  6. Menu 5 — missing fips peer field and ** NEW IDENTITY ** line.
  7. 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)

  1. Treat src/main.c as the source of truth. The functions to compare against are:
  2. Keep this file (plans/menu_gap_analysis.md) as the living checklist; tick rows as the Rust port converges.
  3. 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_print calls 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.