Files
signer/plans/menu_gap_analysis.md
T

260 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Menu Gap Analysis: C `main.c` vs Rust `signer`
**Source of truth:** the C code in [`src/main.c`](../n_signer/src/main.c:1), NOT
[`documents/nsigner_menus.md`](../n_signer/documents/nsigner_menus.md:1) (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`](../signer/src/tui.rs:1) and
[`src/main.rs`](../signer/src/main.rs:1).
Legend: ✅ matches, ⚠️ partial, ❌ missing/divergent.
---
## Menu 1 — Unlock / Mnemonic source
**C** ([`prompt_load_mnemonic_tui`](../n_signer/src/main.c:2789)):
- 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`](../signer/src/main.rs:549)):
- 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`/`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`](../n_signer/src/main.c:2031)):
- `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`](../signer/src/tui.rs:534)):
- **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`](../n_signer/src/main.c:3075)):
- 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`](../signer/src/tui.rs:742)):
- 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`](../n_signer/src/main.c:1898), [`g_main_menu_items`](../n_signer/src/main.c:895)):
- 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`](../signer/src/tui.rs:332), [`MAIN_MENU_ITEMS`](../signer/src/tui.rs:266)):
| 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`](../n_signer/src/main.c:1963)):
- 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`](../signer/src/tui.rs:486)):
| 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`](../n_signer/src/main.c:1804)):
- Full screen clear, top frame
- Iterates `connection_info` entries built from **actual active transports**
([main.c:4086-4190](../n_signer/src/main.c:4086)): 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`](../signer/src/tui.rs:415)):
- **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)
```mermaid
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`](../n_signer/src/main.c:1) as the source of truth. The
functions to compare against are:
- [`prompt_load_mnemonic_tui`](../n_signer/src/main.c:2789) — Menu 1
- [`prompt_named_path_roles`](../n_signer/src/main.c:2031) — Menu 2
- [`prompt_transport_selection`](../n_signer/src/main.c:3075) — Menu 3
- [`render_status`](../n_signer/src/main.c:1898) — Menu 4
- [`tui_approval_cb`](../n_signer/src/main.c:1963) — Menu 5
- [`render_connections`](../n_signer/src/main.c:1804) — Menu 6
2. Keep this file ([`plans/menu_gap_analysis.md`](plans/menu_gap_analysis.md:1))
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.