260 lines
11 KiB
Markdown
260 lines
11 KiB
Markdown
# 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/signer_menus.md`](../n_signer/documents/signer_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: `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`](../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.
|