# 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
  - Use the wordlist exposed by `nostr_core_lib` (no vendored `bip39_english.c` in this repo).
- `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 (aliases: `--name`, `-n`).
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

- ESP32 transport shim interface definition and frame format details.
- NIP-46 relay transport integration timeline.
- Role enrollment UX depth at startup vs. minimal defaults.
- Optional policy granularity beyond same-uid + interactive prompt (e.g. per-verb/per-role session rules).

## 6. Immediate next work

- Add regression tests for prompt-overlay behavior and running-phase hotkeys.
- Expand integration coverage for NIP-04/NIP-44 edge cases and negative-path errors.
- Document and test static artifact size budgets across targets.
- Define MCU transport adapter contract to prepare desktop/firmware parity.

## 7. Transport expansion roadmap

Goal: keep one signer core, swap transports underneath without touching dispatcher, policy, or role layers. The wire contract in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) (4-byte length-prefixed JSON-RPC) stays identical across every transport; only listener and `caller_identity_t` change.

### 7.0 Prerequisite — transport abstraction (Phase T0)

Before adding any new transport, factor a small adapter contract out of [`src/server.c`](../src/server.c) and [`src/main.c`](../src/main.c).

- New header `src/transport.h` declaring an opaque `nsigner_transport_t` with:
  - `accept(listener) -> connection`
  - `recv_frame(connection) -> bytes`
  - `send_frame(connection, bytes)`
  - `peer_identity(connection) -> caller_identity_t`
  - `close(connection)` / `shutdown(listener)`
- Generalize `caller_identity_t` to a tagged union of:
  - `unix_peer { uid, pid, comm }` (current behavior)
  - `qubes { source_qube_name }`
  - `tcp_local { addr }`
  - `tcp_remote { addr, authenticated_pubkey }`
  - `fips { peer_npub }`
  - `usb_serial { device_path, asserted_caller }`
- Move `recv_framed` / `send_framed` from `server.c` and `main.c` into a single shared `transport_frame.c` so client and server share one framing implementation.
- Server main loop becomes transport-agnostic (`while accept; recv; dispatch; send`).
- Tests: extend [`tests/test_integration.c`](../tests/test_integration.c) with a transport-loopback fake to validate the abstraction without binding any real socket.

This refactor is purely internal — no observable change.

### 7.1 Phase T1 — Qubes OS qrexec transport

Use Qubes' native inter-qube primitive instead of inventing one.

- Add a qrexec service script (e.g. `qubes.NsignerRpc`) that execs `nsigner` in a "stdio transport" mode where stdin/stdout carry the existing length-prefixed frame protocol.
- New CLI: `nsigner --listen stdio` (and `nsigner --listen qrexec`, behaving identically; `qrexec` value is for documentation/intent).
- Caller identity comes from qrexec environment (`QREXEC_REMOTE_DOMAIN`) and is mapped to `caller_identity_t.kind=qubes`.
- Reference policy file under `packaging/qubes/policy.d/40-nsigner.policy` showing `ask` / `allow` per source qube.
- No new attack surface inside nsigner: dom0 enforces who can even invoke the service.
- Tests: a unit test that injects fake qrexec env vars and a stdio framing harness; an integration script that documents end-to-end install in a Qubes VM (manual, not in CI).
- Docs: add a "Qubes deployment" section to [`README.md`](../README.md) and to [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).

### 7.2 Phase T2 — TCP transport

Smallest IP-based step; on-ramp for non-Linux clients and for FIPS later.

- New CLI: `nsigner --listen tcp:HOST:PORT` (IPv4 literal) or `nsigner --listen tcp:[IPv6]:PORT`.
- Current behavior: accepts operator-selected local/remote bind addresses (including `[::]` and `fd..`), pending later transport hardening.
- Caller identity for TCP: endpoint address/port in caller descriptor. Approval prompt still mandatory.
- `nsigner list` extended to enumerate active TCP listeners (from internal registry; not from `/proc/net/tcp`).
- Same framing as AF_UNIX path; no protocol changes.
- Tests: integration coverage that spawns a child signer with `--listen tcp:127.0.0.1:0` (and one IPv6 case), captures the bound port, runs the same NIP-04/NIP-44/sign_event matrix as AF_UNIX.
- Docs: extend [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md) section 2 with `tcp:` discovery rules and section 3 confirming framing parity.

Implementation checklist (Tier-1 delivery):

- [x] Parse `--listen tcp:HOST:PORT` in [`src/main.c`](../src/main.c).
- [x] Parse and validate literal IPv4/IPv6 listen targets in [`src/server.c`](../src/server.c).
- [x] Bind/listen non-blocking TCP sockets and run server loop without TUI dependence.
- [x] Keep existing framed JSON-RPC protocol unchanged via shared [`src/transport_frame.c`](../src/transport_frame.c).
- [ ] Add integration test coverage for `tcp:127.0.0.1:PORT` request flow.

### 7.3 Phase T3 — TCP remote with TLS + caller-pubkey auth

Only after T2 is solid.

- New CLI: `nsigner --listen tcp:0.0.0.0:PORT --allow-remote --tls-cert <pem> --tls-key <pem>`.
- Mandatory: TLS for any non-loopback bind. Refuse to start otherwise.
- Caller authentication: client must sign a per-connection challenge with its declared npub (Schnorr/secp256k1) before any signer verb is dispatched. Identity becomes `tcp_remote { addr, authenticated_pubkey }`.
- Failure modes: `transport_tls_required`, `caller_auth_failed`, `caller_auth_timeout` — all surfaced with new error names in dispatcher and documented in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md).
- Approval prompt now displays `caller=npub:abcd…wxyz` instead of `uid:1000`.
- Tests: integration test that exercises happy path, wrong-pubkey, replayed-challenge, expired-challenge.
- Docs: dedicated "Remote TCP deployment" section in `README.md` with strong "do not expose to the public internet without firewalling" warning.

### 7.4 Phase T4 — FIPS substrate integration

FIPS is a *substrate* for an existing TCP listener, not a new transport in nsigner code.

- Deployment topology: nsigner binds a chosen TCP endpoint inside the FIPS network namespace (or on a host where `fips0` is up); peers reach it via `fd00::/8` IPv6 derived from the signer's npub.
- Optional `caller_kind=fips` enrichment: a small sidecar query (`fipsctl show sessions` style) maps the connecting IPv6 address to a peer npub and feeds it into `caller_identity_t.fips { peer_npub }`. If unavailable, fall back to `tcp_remote` identity.
- nsigner does not embed FIPS, does not depend on libfips, and does not require Rust.
- New optional flag: `--peer-id-source fips:/var/run/fips/fips.sock` (path/method TBD per FIPS API).
- Tests: a Docker-compose fixture borrowed from `resources/fips/testing/` that boots two FIPS nodes, runs nsigner on one, runs a Python client (per snippet in [`documents/CLIENT_IMPLEMENTATION.md`](../documents/CLIENT_IMPLEMENTATION.md)) on the other, and exercises the same verb matrix.
- Docs: new [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md) deep-dive describing identity mapping, npub-as-caller, and operator setup. Cross-link from [`README.md`](../README.md) section 7 (Transport).

Execution tasks for initial FIPS trial:

- [x] Deliver T2 TCP listener as FIPS substrate prerequisite.
- [x] Document signer/caller qube deployment flow in [`documents/FIPS_DEPLOYMENT.md`](../documents/FIPS_DEPLOYMENT.md).
- [ ] Add two-node operator validation script (manual) using `fipsctl` + framed JSON-RPC client.
- [ ] Evaluate optional caller identity enrichment from FIPS session metadata.

### 7.5 Phase T5 — USB / serial transport

Two distinct sub-tracks; do not conflate.

- T5a (firmware-side, MCU): ESP32/USB-CDC. Already in the [`firmware/`](../firmware/) track. Same dispatcher; transport adapter is UART read/write loop. `caller_identity_t.kind=usb_serial` with `asserted_caller` because the host claims the identity.
- T5b (host-side optional): `nsigner --listen serial:/dev/ttyACM0,baud=115200`. Useful for desktop signer reachable by a USB-tethered client. Same frame protocol over the serial line. Marks identity as asserted (low trust) and forces approval prompt.
- USB-as-Ethernet (gadget mode, RNDIS/ECM) is **not** a separate transport — it reduces to T2/T3.
- Tests: loopback pty pair (`openpty`) for T5b unit/integration coverage; firmware-side covered in firmware track.

### 7.6 Cross-cutting concerns

Apply once per phase as needed:

- Transport-aware approval prompt: clear visual indication of transport kind and identity (uid vs qube vs npub vs serial-asserted). No silent identity-source confusion.
- Per-transport policy gates: deny-by-default for new identity kinds until operator explicitly enables them in policy.
- Discovery (`nsigner list`) becomes per-transport pluggable (proc/net/unix today, internal registry for tcp, qrexec service announce for qubes, fips peer table for fips).
- Audit logging: include transport kind and identity descriptor in every approval/decision record.
- Error name parity: every new transport introduces only well-named errors (extend the table in [`CLIENT_IMPLEMENTATION.md`](../CLIENT_IMPLEMENTATION.md) section 5).

### 7.7 Decision points (open)

- D1: Land T0 (refactor) before any transport, or in parallel with T1?
- D2: Bundle T2 and T3 as one phase, or hard split (loopback-only first, then remote-with-TLS later)?
- D3: T4 FIPS — embed an explicit `caller_kind=fips` path in nsigner now, or treat FIPS as plain TCP and revisit identity enrichment after a working deployment?
- D4: T5b host-side serial — in scope for desktop nsigner, or strictly firmware track?
- D5: Qubes packaging — ship `packaging/qubes/` artifacts in this repo, or document only and let operators wire it up?
