# Plan (future): Host programs that embed and spawn `n_signer`

Status: design only. Do not implement until [`plans/mnemonic_startup_input.md`](mnemonic_startup_input.md) has landed and been used in at least one host program.

This plan describes how a host program (for example `~/lt/nostr_terminal`, future Bitcoin or SSH frontends) can embed the `nsigner` binary into itself and launch it in a new visible terminal window with the mnemonic pre-loaded and pre-approvals declared, without any new on-disk artifact for `nsigner`.

## 1. Why embed at all

Goals motivating this plan:

1. **Single-binary deployment.** A user installs `nostr_terminal` and gets `nsigner` for free; no separate package manager step, no `$PATH` confusion, no version skew.
2. **Version pinning.** The host always launches the exact `nsigner` it was tested against.
3. **Zero filesystem footprint preserved.** Spawning `nsigner` does not put a copy of the binary anywhere persistent; it runs out of an anonymous in-kernel memory object.
4. **Same security boundary.** `nsigner` continues to run as a separate process with its own address space, mlock'd pages, and approval prompt loop. The host does not become the signer; it merely launches it.
5. **Reusable across host programs.** Any program in the user's `~/lt/*` ecosystem can adopt the same helper.

Non-goals (explicit):

- Running `nsigner` *inside* the host process. That collapses the security boundary and is rejected. See [`documents/SECURITY.md`](../documents/SECURITY.md) "One process. One terminal. One human."
- Hiding the `nsigner` window. The TUI is the trust anchor; it must be visible and attended.
- Making `nsigner` a daemon. Same reason.

## 2. Architecture overview

```text
+----------------------------------------------------------------+
| host program (e.g. nostr_terminal)                             |
|                                                                |
|  +-- embedded blob ---------------------------------+          |
|  |   _binary_nsigner_start ... _binary_nsigner_end  |          |
|  |   (the full statically-linked nsigner ELF)       |          |
|  +--------------------------------------------------+          |
|                                                                |
|  user types mnemonic into host's TUI                           |
|     |                                                          |
|     v                                                          |
|  nsigner_spawn_embedded(...)                                   |
|     |                                                          |
|     | 1. memfd_create(MFD_CLOEXEC)                             |
|     | 2. write(memfd, blob, blob_len)                          |
|     | 3. pipe2(stdin_pipe, O_CLOEXEC)                          |
|     | 4. fork() terminal emulator wrapper                      |
|     |        child: dup2 stdin_pipe[0] -> 0                    |
|     |               execvp("kitty","-e","/proc/self/fd/<memfd>"|
|     |                       --listen unix --name <chosen>      |
|     |                       --mnemonic-stdin                   |
|     |                       --preapprove ...)                  |
|     | 5. parent: write(stdin_pipe[1], mnemonic); close + wipe  |
|     | 6. return chosen socket name to caller                   |
+----------------------------------------------------------------+
                                |
                                v   (new graphical terminal window)
+----------------------------------------------------------------+
| nsigner (separate process)                                     |
|   - reads mnemonic from FD 0, closes FD 0                      |
|   - binds @nsigner_<chosen> abstract unix socket               |
|   - shows status TUI                                           |
|   - applies pre-approvals from --preapprove                    |
+----------------------------------------------------------------+
                                ^
                                |  (host can now connect)
+----------------------------------------------------------------+
| host re-uses client/nsigner_client.{c,h} to issue requests     |
+----------------------------------------------------------------+
```

## 3. Build-time integration

### 3.1 Producing the blob

Add a Makefile rule (in `n_signer`'s Makefile) that exposes the binary as a relocatable object usable by other projects:

```make
nsigner_blob.o: nsigner
	$(LD) -r -b binary -o $@ $<
	# results in symbols:
	#   _binary_nsigner_start
	#   _binary_nsigner_end
	#   _binary_nsigner_size
```

Alternatives considered:

- `xxd -i nsigner > nsigner_blob.h` — produces a C header with a `unsigned char` array. Works, but adds compile time linear in blob size and blocks ccache usefulness.
- `objcopy -I binary -O elf64-x86-64 -B i386 nsigner nsigner_blob.o` — same effect as `ld -r -b binary` on most toolchains. Either is fine.

### 3.2 Embedding into the host

The host program adds these lines to its build:

```make
HOST_OBJS += nsigner_blob.o nsigner_spawn.o
```

and links them into its final executable. The blob lives in `.data` (read-only on most toolchains) and adds roughly the size of the `nsigner` binary to the host (currently ~1–3 MB for the musl static build).

### 3.3 Symbol declaration

```c
extern const unsigned char _binary_nsigner_start[];
extern const unsigned char _binary_nsigner_end[];
/* size = _binary_nsigner_end - _binary_nsigner_start */
```

## 4. Runtime spawn helper API

New file pair: `client/nsigner_spawn.h` and `client/nsigner_spawn.c`.

### 4.1 Public API sketch

```c
typedef struct {
    /* required */
    const char *mnemonic;        /* will be written to child stdin pipe and then wiped */
    size_t      mnemonic_len;

    /* optional */
    const char *const *preapprove_specs;  /* array of "caller=...,nostr_index=N" strings */
    size_t             preapprove_count;

    const char *socket_name;     /* if NULL, helper picks a random BIP-39 pair */
    const char *terminal_hint;   /* "kitty"|"foot"|"alacritty"|"xterm"|... or NULL = autodetect */

    /* outputs */
    char  *out_socket_name;      /* caller-supplied buffer */
    size_t out_socket_name_size;

    pid_t *out_terminal_pid;     /* may be NULL */
} nsigner_spawn_opts_t;

/* Returns 0 on success, negative on error. On success, the embedded nsigner
 * binary has been written to a memfd, the terminal window has been launched,
 * the mnemonic has been written into the child's stdin pipe, and the local
 * `mnemonic` buffer has been zeroized.
 *
 * After return, the host can connect to the socket using
 *   client/nsigner_client.{c,h}
 * with the chosen socket name in opts->out_socket_name.
 */
int nsigner_spawn_embedded(nsigner_spawn_opts_t *opts);
```

Optional readiness primitive (post-MVP): a `nsigner_spawn_wait_ready()` call that polls the abstract socket until it accepts a connection or until a timeout, so the host doesn't race the child's bind step.

### 4.2 Internal pipeline

1. Validate inputs: non-empty mnemonic, sane buffer sizes, `out_socket_name` non-NULL.
2. Pick a socket name: random BIP-39 word pair if not provided. Reuses the same name-generation routine that `nsigner` itself uses; consider extracting it into a shared header `src/socket_name.h` already exists ([`src/socket_name.c`](../src/socket_name.c:1)) and exposing the generator publicly.
3. Create memfd: `memfd_create("nsigner", MFD_CLOEXEC)`. On older kernels (no `memfd_create`), return an error rather than falling back to disk — the whole point is no filesystem footprint. (Linux 3.17+ has memfd; the project's deployment targets all qualify.)
4. Write the embedded blob to the memfd. `lseek` back to zero is unnecessary because `fexecve`/`execveat` does not care about offset.
5. Create stdin pipe: `pipe2(stdin_pipe, O_CLOEXEC)`. Mark only the read end as inheritable in the child (clear `FD_CLOEXEC` on the read end after `fork`).
6. Pick a terminal emulator. Detection order: `$NSIGNER_TERMINAL` env override, then `terminal_hint`, then a built-in priority list:

   | Emulator | Run-this flag | Notes |
   |---|---|---|
   | `kitty` | `--detach -- <argv>` | preserves stdin |
   | `foot` | `-- <argv>` | preserves stdin |
   | `alacritty` | `-e <argv>` | preserves stdin |
   | `wezterm` | `start -- <argv>` | preserves stdin |
   | `xterm` | `-e <argv>` | preserves stdin (legacy reliable) |
   | `gnome-terminal` | **avoided** | dbus-launches, drops stdin/FDs |
   | `konsole` | `-e <argv>` | mostly preserves stdin |

   If none found, return an error and let the host print a hint to install one.
7. Build child argv:
   - `[0] = chosen_terminal`
   - `[1..]` = the terminal's "exec" flag(s) and separator
   - then `[N+0] = "/proc/self/fd/<memfd>"` (path the kernel exposes for the memfd, valid only inside the child's process tree — no on-disk file is created)
   - then `--listen unix --name <chosen> --mnemonic-stdin`
   - then each `--preapprove <spec>` pair
8. `fork()`. In the child:
   - `dup2(stdin_pipe[0], 0)` (replace stdin with the read end)
   - `close(stdin_pipe[1])`
   - clear `FD_CLOEXEC` on the memfd so `/proc/self/fd/<memfd>` resolves after `execvp` runs the terminal — the terminal then runs `nsigner` via the inherited memfd path
   - `execvp(chosen_terminal, child_argv)`
9. In the parent:
   - `close(stdin_pipe[0])`
   - `write(stdin_pipe[1], mnemonic, mnemonic_len)`
   - `write(stdin_pipe[1], "\n", 1)`
   - `close(stdin_pipe[1])`
   - `secure_memzero` over the host's mnemonic buffer
   - close the memfd in the parent (the child kernel reference keeps it alive)
   - copy the chosen socket name into `out_socket_name`
   - return 0

### 4.3 Failure modes and cleanup

Every failure path must:

- close any FDs already opened (memfd, both pipe ends)
- zeroize the mnemonic buffer if it was already copied locally
- not leave a child process behind: if `fork` succeeded but a later step failed, `waitpid` the child after `kill(SIGTERM)`

A short error-string accessor (`const char *nsigner_spawn_strerror(int)`) keeps callers from needing to know the internal codes.

## 5. Security review (to perform before implementation)

These items are flagged now so the implementation phase doesn't drift:

1. **Confirm memfd contents are not exposed to other processes.** `/proc/self/fd/<memfd>` is per-process; another process cannot dereference another's `/proc/self/fd/N`. Document that fact in the code comments and in [`documents/SECURITY.md`](../documents/SECURITY.md).
2. **Confirm `MFD_CLOEXEC` actually closes on the *outer* exec.** We need the memfd open across `fork`+`execvp(terminal)` but want it gone from the terminal emulator's children that aren't `nsigner`. Tested behavior: clear `FD_CLOEXEC` only on this one FD before `execvp` so the terminal inherits it; the terminal itself doesn't close arbitrary FDs in its child by default but a few do. Document the matrix.
3. **Sealing.** Optionally `fcntl(memfd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW)` before exec, so even a compromised child cannot rewrite the binary it is about to execute.
4. **Pre-approval defaults.** The helper must NOT silently inject a default `--preapprove` for the host's uid. The host must declare every approval explicitly. This keeps the `documents/SECURITY.md` "deny by default" guarantee intact.
5. **Mnemonic in pipe buffer.** The kernel pipe buffer (4 KiB) holds the mnemonic for the moment between parent's `write` and child's `read`. This is in-kernel anonymous memory, comparable to a `mlock`'d page in lifetime, and is freed when both ends close. Acceptable.
6. **Mnemonic in environment of the host.** The helper does not put the mnemonic in `envp`. It only goes through the pipe.
7. **Host trust elevation.** When the host calls this helper, the host program joins the trust chain (it already had the mnemonic). Document that in [`documents/SECURITY.md`](../documents/SECURITY.md) §2.3 trust-anchors table.
8. **Terminal emulator trust.** The chosen terminal emulator is now part of the trust chain (it sees stdin and could capture it). Document; recommend `kitty`/`foot`/`alacritty`/`xterm` and warn against modifying via plugins/config that record stdin.

## 6. Documentation deliverables

- `documents/SPAWN.md`: how host programs embed `nsigner_blob.o` and use `nsigner_spawn.{c,h}`.
- Update [`documents/SECURITY.md`](../documents/SECURITY.md) §2.3 with the host-program and terminal-emulator trust-chain rows.
- Update [`README.md`](../README.md) §3.1 with a one-paragraph "Running embedded inside a host program" note pointing here.
- New worked example `examples/spawn_and_sign.c`: takes a mnemonic from its own argv (for the example only — clearly labeled "EXAMPLE: argv mnemonic, not for production"), spawns `nsigner` via the helper, then issues `get_public_key` over the resulting socket and prints the result. This demonstrates the full pattern in ~80 lines.

## 7. Testing strategy

Tests are inherently more involved because they require spawning a real terminal emulator. Recommended approach:

1. **Unit tests for the helper** (`tests/test_spawn_unit.c`):
   - argv builder produces the expected list given a fixture options struct.
   - terminal autodetect returns the correct emulator given a fake `$PATH`.
   - the memfd write produces a payload whose first 4 bytes are `\x7fELF` (sanity).
2. **Integration test under a headless terminal** (`tests/test_spawn_integration.c`):
   - use `xvfb-run` plus `xterm -e` (or `foot --headless`-ish modes if available) so CI can spawn a window without a display server.
   - the test passes a known mnemonic, waits for the abstract socket to appear, calls `get_public_key`, asserts the returned pubkey matches the BIP-39 vector for that mnemonic.
   - this test is gated on a build flag because not every CI environment has a usable headless emulator; `make test-spawn` rather than the default `make test`.
3. **Manual smoke checklist** in `documents/SPAWN.md` covering the four supported emulators.

## 8. Open questions to resolve before implementation

- Should the helper live in `client/` (next to `nsigner_client.{c,h}`) or in a new top-level `embed/` directory? Recommendation: `client/` keeps related host-side code together.
- Should the embedded blob be compressed (e.g. zstd-decompress on the fly into the memfd)? Probably not for the first version — extra dependency, marginal size benefit on already-small statically-linked `nsigner`.
- How does this interact with code signing on platforms that enforce it (Windows, macOS, signed Linux distros)? Linux generally allows `fexecve` of memfd content; macOS would require a different path. The plan is Linux-only for now; document that.
- Should `nsigner` itself learn a `--print-socket-name-on-startup` flag (one line to a designated FD) so the spawn helper doesn't have to choose the name? Useful future addition; keep it as a follow-up after the memfd path proves out.

## 9. Acceptance criteria (when the time comes to implement)

- [ ] `make nsigner_blob.o` produces a relocatable object usable by host projects.
- [ ] `client/nsigner_spawn.{c,h}` compiles cleanly and links into a host program with no extra dependencies.
- [ ] `examples/spawn_and_sign.c` runs end-to-end and prints a valid pubkey.
- [ ] No file is created on disk during a successful spawn (verified by `lsof` snapshot or `inotifywait` on `/tmp`, `/run/user/$UID`, `$HOME`).
- [ ] Host's mnemonic buffer is wiped on the path to `execvp`.
- [ ] Pre-approvals declared by the host appear in the spawned `nsigner`'s policy table without prompting.
- [ ] Documentation updates landed in `documents/SPAWN.md`, [`documents/SECURITY.md`](../documents/SECURITY.md), and [`README.md`](../README.md).
- [ ] Tests in `tests/test_spawn_unit.c` pass on plain CI; `tests/test_spawn_integration.c` passes under `xvfb-run` on the spawn-CI lane.

## 10. Relationship to other plans

- Builds on [`plans/mnemonic_startup_input.md`](mnemonic_startup_input.md) — the embedded helper consumes `--mnemonic-stdin`.
- Compatible with [`plans/deny_by_default_approvals.md`](deny_by_default_approvals.md) — every host-issued approval still goes through `--preapprove`.
- Compatible with [`plans/caller_token_identity.md`](caller_token_identity.md) — host identity is conveyed via `unix_peer` over the abstract socket like any other client.
- Companion to [`plans/auth_envelope_other_transports.md`](auth_envelope_other_transports.md): a host that spawns `nsigner` may also wish to forward authenticated requests on its behalf; out of scope here.
