9.4 KiB
9.4 KiB
nsigner Implementation Plan
This document is the implementation roadmap for n_signer.
Authoritative behavior documentation: 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.candsrc/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(defaultE). - New module function
mnemonic_generate(int word_count, char *out, size_t out_len)insrc/mnemonic.c:getrandom(2)for entropy (16 bytes for 12 words, 32 bytes for 24).- Compute SHA-256 of entropy → first
entropy_bits / 32checksum 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: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_validatefunction (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, formatsnsigner_<w1>_<w2>.
- BIP-39 English wordlist
- Reuse the wordlist already linked via
nostr_core_libif exposed; otherwise vendorsrc/bip39_english.cas a 2048-entryconst char *[].
- Reuse the wordlist already linked via
src/server.cbind logic- Accept the resolved name from caller.
- Bind once. On
EADDRINUSEand no--socket-nameoverride, regenerate a fresh random name and retry up to 8 times. With override, fail immediately and report the override conflict.
src/main.cintegration- 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.
- Resolve name precedence:
nsigner listsubcommand- 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.
- Reads
- 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_runso 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-nameis unchanged; it remains the deterministic override for scripts and tests.
4. Decisions log
2026-05-02 — Mnemonic generation at startup
- Startup offers
[E]nteror[G]eneratechoice; default isE. - Generated mnemonic uses
getrandom(2)+ BIP-39 English wordlist, default 12 words. - Mnemonic is displayed exactly once with a "write this down" warning.
- No confirmation step — user is trusted to copy the words; we proceed straight to session use.
- 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
- Multiple
nsignerinstances can run concurrently on one host. - Default socket name is
@nsigner_<bip39_word1>_<bip39_word2>chosen randomly at each launch. --socket-name <name>overrides the random pick for tests and scripted usage.nsigner listenumerates running signers via/proc/net/unixfiltered onnsigner_prefix.- 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
- Single foreground process replaces daemon + TUI + client multi-binary model
- No config files — all state entered at runtime, lives in RAM only
- Abstract namespace sockets replace filesystem sockets
- Control socket eliminated — TUI is in-process
- Policy simplified to same-uid default + interactive approval
- Crash = total wipe is a security feature, not a limitation
- Same core modules target both Linux desktop and ESP32 MCU
2026-05-02 — Earlier decisions (retained)
role_pathmust be pre-registered (no ad-hoc derivation)nostr_indexnaming (notrole_index)- Multi-curve support from start (
secp256k1,ed25519,x25519) - 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 +
listsubcommand)