# 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 ''` 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__` (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__`. - 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__` 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__` chosen randomly at each launch. 3. `--socket-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)