Files
n_signer/plans/nsigner.md

181 lines
9.4 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.

# nsigner Implementation Plan
This document is the implementation roadmap for `n_signer`.
Authoritative behavior documentation: [README.md](../README.md).
## 1. Architecture pivot: program not daemon
This plan adopts a single foreground program model instead of a daemon model to reduce operational complexity and eliminate persistent runtime artifacts.
- Single foreground process, not a background daemon
- No config files — all state entered at runtime via TUI
- No control socket — TUI is built into the program
- No separate binaries — one binary with subcommands
- Abstract namespace sockets — no filesystem artifacts
## 2. Module inventory (what exists, what stays, what goes)
| Module | Fate |
|---|---|
| `src/secure_mem.{h,c}` | **KEEP** unchanged |
| `src/mnemonic.{h,c}` | **KEEP** unchanged |
| `src/role_table.{h,c}` | **KEEP** data structures, **REMOVE** file-loading code |
| `src/selector.{h,c}` | **KEEP** unchanged |
| `src/enforcement.{h,c}` | **KEEP** unchanged |
| `src/dispatcher.{h,c}` | **KEEP** unchanged |
| `src/cjson/` | **KEEP** unchanged |
| `src/policy.{h,c}` | **REFACTOR**: remove file loading, add interactive approval, hardcode same-uid default |
| `src/daemon.{h,c}` | **REPLACE** with `src/server.{h,c}`: abstract namespace socket, poll-based, integrated with TUI |
| `src/control.{h,c}` | **DELETE** (control socket no longer needed) |
| `src/tui.c` | **DELETE** as separate binary (functionality moves into `src/main.c`) |
| `src/client.c` | **DELETE** as separate binary (becomes subcommand in `src/main.c`) |
| `src/main.c` | **REWRITE**: startup TUI + server loop + status display + client subcommand |
| `config/` | **DELETE** entirely |
| `tests/test_daemon_integration.c` | **DELETE** or rewrite |
| `tests/test_full_flow.c` | **REWRITE** for new architecture |
| `tests/test_policy.c` | **UPDATE** for new policy API |
## 3. Implementation phases (new)
### Phase A — Cleanup and removal
- Delete `config/`, `src/control.{h,c}`, `src/tui.c`, `src/client.c`
- Remove file-loading functions from `src/role_table.c` and `src/policy.c`
- Remove old daemon integration tests
### Phase B — Server module (replaces daemon)
- Add `src/server.{h,c}`: abstract namespace socket, poll-based accept loop
- Keep peer-cred extraction behavior on abstract socket
- Add integration point for TUI approval callbacks
### Phase C — Unified main with built-in TUI
- Startup phase: mnemonic prompt (echo off), word count confirm, optional role enrollment
- Transition to running phase: status display, activity log, hotkey handling
- Approval prompt overlay when policy requires it
- Client subcommand: `nsigner client '<json>'` connects to abstract socket, sends request, prints response
- Signal handling: `SIGINT`/`SIGTERM` → zeroize and exit
### Phase D — Policy simplification
- Default policy: same-uid as process = allow all verbs on all roles, no prompt
- Unknown caller: display approval prompt in TUI, user decides per-request or per-session
- No file-based policy at all
### Phase E — Integration test
- Single test that launches the program (with mnemonic piped via stdin for automation), sends requests via client subcommand, verifies responses
### Phase G2 — Mnemonic generation at startup
Goal: let the user generate a brand-new BIP-39 mnemonic at startup instead of typing an existing one. The generated mnemonic is shown once and used for the session; no confirmation step is required.
Steps:
- Extend startup TUI: prompt user with `[E]nter existing mnemonic` / `[G]enerate new mnemonic` (default `E`).
- New module function `mnemonic_generate(int word_count, char *out, size_t out_len)` in [`src/mnemonic.c`](../src/mnemonic.c:1):
- `getrandom(2)` for entropy (16 bytes for 12 words, 32 bytes for 24).
- Compute SHA-256 of entropy → first `entropy_bits / 32` checksum bits.
- Concatenate entropy + checksum bits → slice into 11-bit groups.
- Look up each group in the BIP-39 English wordlist → space-separated mnemonic.
- Zeroize the entropy buffer immediately after use.
- Default word count: 12 (no extra prompt).
- TUI display:
- Numbered word list (1..12).
- Loud warning: "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN."
- "Press any key to continue" — explicit no-confirmation policy per spec.
- After display, the in-memory mnemonic is fed straight into the existing seed-derivation path; no separate buffer or persistence layer is introduced.
- Tests in [`tests/test_mnemonic.c`](../tests/test_mnemonic.c:1):
- `mnemonic_generate(12)` returns 12 valid BIP-39 English words.
- `mnemonic_generate(24)` returns 24 valid BIP-39 English words.
- Output validates against the existing `mnemonic_validate` function (round-trip).
- Two consecutive calls return different mnemonics with overwhelming probability.
### Phase H — Multi-instance signer naming (random BIP-39)
Goal: allow multiple `nsigner` processes to coexist on one host by giving each instance a unique, human-readable abstract socket name.
Approach: at startup, pick two random BIP-39 English words and bind to `@nsigner_<word1>_<word2>` (e.g. `@nsigner_hairy_dog`).
Steps:
- New module `src/socket_name.{h,c}`
- `int socket_name_random(char *out, size_t out_len);`
- Calls `getrandom(2)` for 3 bytes (24 bits), splits into two 11-bit indices, looks up words from the BIP-39 English wordlist, formats `nsigner_<w1>_<w2>`.
- BIP-39 English wordlist
- Reuse the wordlist already linked via `nostr_core_lib` if exposed; otherwise vendor `src/bip39_english.c` as a 2048-entry `const char *[]`.
- `src/server.c` bind logic
- Accept the resolved name from caller.
- Bind once. On `EADDRINUSE` and no `--socket-name` override, regenerate a fresh random name and retry up to 8 times. With override, fail immediately and report the override conflict.
- `src/main.c` integration
- Resolve name precedence: `--socket-name` (explicit) > random pick.
- After mnemonic acceptance, derive/pick the name and pass it to the server.
- TUI startup banner shows the friendly name and the socket address prominently so the user knows what to point clients at.
- `nsigner list` subcommand
- Reads `/proc/net/unix`, filters entries whose path starts with `\0nsigner_`, prints them as `@nsigner_<w1>_<w2>` along with inode/state if useful.
- No protocol change; purely a discovery helper.
- Tests
- `tests/test_socket_name.c` — format check, both indices land in `[0, 2048)`, two consecutive calls produce different names with overwhelming probability.
- `tests/test_integration.c` — launch with explicit `--socket-name nsigner_test_run` so the integration test is deterministic and isolated.
Privacy/UX notes:
- Random names leak nothing about the seed; the trade-off is the user must read the banner each launch to know which socket to address.
- ~4.2M combinations × small concurrent process count = collision essentially never observed in practice; retry path exists for correctness.
- Behavior of `--socket-name` is unchanged; it remains the deterministic override for scripts and tests.
## 4. Decisions log
### 2026-05-02 — Mnemonic generation at startup
1. Startup offers `[E]nter` or `[G]enerate` choice; default is `E`.
2. Generated mnemonic uses `getrandom(2)` + BIP-39 English wordlist, default 12 words.
3. Mnemonic is displayed exactly once with a "write this down" warning.
4. **No confirmation step** — user is trusted to copy the words; we proceed straight to session use.
5. The in-memory lifecycle is identical to a typed mnemonic (same secure buffer, same crash-wipe semantics).
### 2026-05-02 — Random per-launch signer naming
1. Multiple `nsigner` instances can run concurrently on one host.
2. Default socket name is `@nsigner_<bip39_word1>_<bip39_word2>` chosen randomly at each launch.
3. `--socket-name <name>` overrides the random pick for tests and scripted usage.
4. `nsigner list` enumerates running signers via `/proc/net/unix` filtered on `nsigner_` prefix.
5. Random naming was chosen over deterministic-from-mnemonic for v1 to avoid leaking any seed-derived identifier in the abstract namespace; deterministic naming may be revisited later.
### 2026-05-02 — Program not daemon pivot
1. Single foreground process replaces daemon + TUI + client multi-binary model
2. No config files — all state entered at runtime, lives in RAM only
3. Abstract namespace sockets replace filesystem sockets
4. Control socket eliminated — TUI is in-process
5. Policy simplified to same-uid default + interactive approval
6. Crash = total wipe is a security feature, not a limitation
7. Same core modules target both Linux desktop and ESP32 MCU
### 2026-05-02 — Earlier decisions (retained)
- `role_path` must be pre-registered (no ad-hoc derivation)
- `nostr_index` naming (not `role_index`)
- Multi-curve support from start (`secp256k1`, `ed25519`, `x25519`)
- [README.md](../README.md) is authoritative behavior spec
## 5. Open questions
- Automated test strategy for interactive TUI (stdin pipe? expect-style? separate test mode flag?)
- ESP32 transport shim interface definition
- NIP-46 relay transport integration timeline
- Role enrollment UX: how many questions at startup vs. "just use main = index 0"?
- Lock-without-exit: needed? Or is quit-and-relaunch sufficient?
## 6. Immediate next work
- Execute Phase A (cleanup)
- Execute Phase B (server module)
- Execute Phase C (unified main)
- Execute Phase D (policy simplification)
- Execute Phase E (integration test)
- Execute Phase G2 (mnemonic generation at startup)
- Execute Phase H (multi-instance random naming + `list` subcommand)