# n-OS-tr Identity Subsystem

> **Status:** Design. This document describes how n-OS-tr turns a single 12-word BIP-39 mnemonic into every key the OS and its applications need, and how the **identity agent** exposes those keys to the rest of the system.
>
> Companion docs: [`plans/threat_model.md`](threat_model.md) for the privacy contract this must satisfy, and [`plans/nostr_config_projection.md`](nostr_config_projection.md) for how the derived main key is used to fetch configuration.

## 1. Concept

n-OS-tr has one piece of root secret material: **a 12-word BIP-39 mnemonic**. Every key the system needs — your public identity, your local relay admin key, your mesh node identity, your blob-store key — is deterministically derived from that mnemonic via [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md).

The user enters (or generates) the mnemonic at boot. A single long-running process — the **identity-agent** — holds the mnemonic in locked memory for the session, derives keys on demand, and signs events on behalf of apps via [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md). No app and no file ever sees the mnemonic or a raw private key.

On shutdown, the identity-agent zeroes the memory, and the ephemeral rootfs disappears. The mnemonic only ever existed in RAM.

## 1.1 Crypto foundation: `nostr_core_lib`

The identity-agent does **not** reimplement any Nostr cryptography. It is built on top of [`includes/nostr_core_lib`](../includes/nostr_core_lib/), a C library already vendored into this repository that provides:

| Capability needed by identity-agent        | `nostr_core_lib` symbol / header                                                       | NIP     |
|--------------------------------------------|----------------------------------------------------------------------------------------|---------|
| Keypair generation (entropy source)        | [`nostr_generate_keypair`](../includes/nostr_core_lib/nostr_core/nip006.h:20)          | n/a     |
| BIP-39 mnemonic generation + NIP-06 derivation | [`nostr_generate_mnemonic_and_keys`](../includes/nostr_core_lib/nostr_core/nip006.h:21) | NIP-06  |
| NIP-06 derivation from existing mnemonic   | [`nostr_derive_keys_from_mnemonic`](../includes/nostr_core_lib/nostr_core/nip006.h:24) | NIP-06  |
| Input type detection (hex vs nsec vs mnemonic) | [`nostr_detect_input_type`](../includes/nostr_core_lib/nostr_core/nip006.h:26)     | n/a     |
| bech32 encoding/decoding (nsec/npub)       | [`nostr_key_to_bech32`](../includes/nostr_core_lib/nostr_core/nostr_core.h), [`nostr_decode_nsec`](../includes/nostr_core_lib/nostr_core/nip006.h:27) | NIP-19  |
| Event creation + signing                   | [`nostr_create_and_sign_event`](../includes/nostr_core_lib/nostr_core/nostr_core.h)    | NIP-01  |
| NIP-44 encrypt / decrypt (for self-encrypted config) | [`nostr_nip44_encrypt`](../includes/nostr_core_lib/nostr_core/nip044.h:28), [`nostr_nip44_decrypt`](../includes/nostr_core_lib/nostr_core/nip044.h:62) | NIP-44  |
| NIP-46 signer session (serve requests)     | [`nostr_nip46_signer_session_init`](../includes/nostr_core_lib/nostr_core/nip046.h:163), [`nostr_nip46_signer_handle_request`](../includes/nostr_core_lib/nostr_core/nip046.h:169) | NIP-46  |
| NIP-46 bunker URL publishing (external signer inbound path) | [`nostr_nip46_signer_create_bunker_url`](../includes/nostr_core_lib/nostr_core/nip046.h:172) | NIP-46  |
| NIP-46 client session (consume remote bunker) | [`nostr_nip46_client_session_init`](../includes/nostr_core_lib/nostr_core/nip046.h:145), [`nostr_nip46_client_sign_event`](../includes/nostr_core_lib/nostr_core/nip046.h:158) | NIP-46  |
| Relay pool + NIP-42 auth for signer relay  | [`nostr_relay_pool_create`](../includes/nostr_core_lib/nostr_core/nostr_core.h), [`nostr_relay_pool_set_auth`](../includes/nostr_core_lib/nostr_core/nostr_core.h) | NIP-42  |
| Library-wide log callback (no on-disk logs) | [`nostr_set_log_callback`](../includes/nostr_core_lib/nostr_core/nostr_log.h)         | n/a     |

Consequence: the identity-agent is a thin C daemon that wires these primitives to systemd, to a local Unix-domain transport, and to the privacy-hardener's memory and shutdown constraints. The crypto has been written, reviewed, and unit-tested in [`includes/nostr_core_lib/tests`](../includes/nostr_core_lib/tests/). We do not touch it.

The Rust- and Python-binding libraries mentioned in §5.2 are thin FFI wrappers over the same library.

## 2. Derivation scheme

n-OS-tr uses the exact NIP-06 derivation exposed by `nostr_core_lib` via [`nostr_derive_keys_from_mnemonic(mnemonic, account, priv, pub)`](../includes/nostr_core_lib/nostr_core/nip006.h:24), where `account` is the role index. This matches both the nostr-tools JavaScript reference implementation in [`/home/user/lt/client/www/tools.html`](../../client/www/tools.html) (line 944) and the standard [NIP-06 specification](https://github.com/nostr-protocol/nips/blob/master/06.md).

The derivation path is:

```
m / 44' / 1237' / index' / 0 / 0
```

where:
- `44'` — BIP-44 purpose
- `1237'` — SLIP-0044 coin type for Nostr
- `index'` — the role index (see table below)
- `0 / 0` — change and address, always zero

Given a 12-word mnemonic, each integer `index` produces a distinct 32-byte secp256k1 private key, from which Nostr npub/nsec are encoded.

### 2.1 The role table (schedule)

Indices are assigned to fixed roles. Index 0 is stable forever; higher indices are allocated as roles are needed. New apps request a new index at *install time*, not at runtime, so the schedule is discoverable and forward-compatible.

| Index | Role name      | Used by                                                                 | Stability  |
|-------|----------------|-------------------------------------------------------------------------|------------|
| `0`   | `main`         | User's public Nostr identity. Profile, posts, social graph, config sig. | Immutable. |
| `1`   | `throwaway`    | Test/experimental account. Safe to publish noise from.                  | Stable.    |
| `2`   | `c-relay-admin`| Admin key for the user's local [`c-relay`](../includes/c-relay/).       | Stable.    |
| `3`   | `ginxsom`      | Server key for the user's local [`ginxsom`](../includes/ginxsom/).      | Stable.    |
| `4`   | `fips`         | Mesh node identity for [`fips`](../includes/fips/). Yields fips IPv6.   | Stable.    |
| `5`   | reserved       | Future: personal Nostr DM relay / gossip cache.                         | Reserved.  |
| `6`   | reserved       | Future: signer key for system audit events.                             | Reserved.  |
| `7+`  | unallocated    | Assigned by future apps via the registration process in §2.3.           | Dynamic.   |

> **Index 0 stability:** Once a user has published anything under their main key, index 0 can never be renumbered. That's why `main` gets index 0 forever. All other role assignments can in principle be renumbered before first use, but once shipped in a released n-OS-tr image they must not be changed without a major version bump.

### 2.2 Deriving a public identifier from a key

Once a private key is derived at an index, standard Nostr encodings apply. In `nostr_core_lib` these are all available directly:

| Want                       | `nostr_core_lib` call                                                                 |
|---------------------------|----------------------------------------------------------------------------------------|
| `npub` (bech32 pubkey)    | `nostr_key_to_bech32(public_key, "npub", out)`                                         |
| `nsec` (bech32 privkey)   | `nostr_key_to_bech32(private_key, "nsec", out)`                                        |
| `hex` pubkey              | produced directly by [`nostr_derive_keys_from_mnemonic`](../includes/nostr_core_lib/nostr_core/nip006.h:24) as 32 bytes |
| `hex` privkey             | produced directly by [`nostr_derive_keys_from_mnemonic`](../includes/nostr_core_lib/nostr_core/nip006.h:24) as 32 bytes |
| FIPS mesh IPv6            | `fd` + first 15 bytes of `sha256(pubkey_bytes)` — implemented locally in identity-agent; the definition matches [`tools.html`](../../client/www/tools.html:750) `pubkeyHexToFipsIpv6()` |

No identity-agent code ever touches raw secp256k1 arithmetic. Everything goes through the library.

### 2.3 Role registration protocol

When a new app wants its own Nostr identity, it does not pick an index itself. It calls the identity-agent:

```
identity-agent register --service=my-app --justification="needs a Nostr key for X"
```

The agent:
1. Looks up the currently allocated schedule (see §4).
2. Allocates the next unused index.
3. Returns the npub (never the nsec or the mnemonic).
4. Persists the name → index mapping **as a signed Nostr config event** under the main key (kind described in [`plans/nostr_config_projection.md`](nostr_config_projection.md)), so the mapping is itself part of the user's portable config.

Because the mapping is on Nostr, it travels with the user, not with the machine. Booting on a different n-OS-tr machine reconstructs the same role table.

## 3. Boot-time UX

When n-OS-tr boots, before any network or user session, it runs the identity-agent as a systemd service, which presents the first-screen flow on whatever console the device has (TTY, framebuffer, LCD HAT).

### 3.1 Three flows

```mermaid
flowchart TB
    A[Boot] --> Q{Identity source?}
    Q -->|Enter mnemonic| E[Enter 12 words]
    Q -->|Generate new| G[Generate 12 words]
    Q -->|Import file| F[Import from /media/*/mnemonic.txt]
    Q -->|Connect bunker| B[NIP-46 bunker URL]

    E --> V[BIP-39 checksum validate]
    G --> D[Display once, require confirmation]
    F --> W[Read once, wipe source file]

    V --> L[Load mnemonic into locked memory]
    D --> L
    W --> L
    B --> RS[Use remote signer,<br/>no local mnemonic]

    L --> R[Ready: expose keys + signer]
    RS --> R
```

### 3.2 Flow details

**Flow 1 — "I have a mnemonic, let me type it."**
- 12 words entered via keyboard (amd64 TTY) or Waveshare-LCD button sequence (Pi Zero).
- Each word is validated against the BIP-39 English wordlist as typed. Typos are rejected before the full phrase is submitted.
- Checksum validated on submit. If invalid, clear the buffer and prompt again.
- On success, loaded into `mlock()`-ed memory.

**Flow 2 — "Generate a new one for me."**
- Entropy sourced from `getrandom(2)` + hardware RNG where available.
- 12 words are displayed *once*, full-screen, with an emphatic "write these down now" message.
- User confirms by retyping (or on Pi: scrolling and clicking through) 3 random word positions (e.g., word 4, word 9, word 11) to ensure they actually recorded the phrase.
- Only after confirmation does boot proceed. If the user reboots without confirming, they have generated nothing of value — no identity has been "committed to" yet.

**Flow 3 — "Import from removable media."**
- At boot, identity-agent scans `/media/*/mnemonic.txt` for a single candidate file.
- Reads it, validates BIP-39 checksum.
- **Wipes the source file** (`shred -u` on ext4 or equivalent on FAT) so that unplugging the stick does not carry the plaintext mnemonic off the machine. User is informed that the stick no longer contains the mnemonic and they must have a backup.
- Loaded into locked memory.

**Flow 4 — "Use an external signer."**
- User provides a [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md) bunker URL (hardware wallet, phone app, remote machine).
- identity-agent does not hold any mnemonic. All signing is forwarded to the external bunker.
- Key derivation for non-signing use (e.g., deriving a fips IPv6) proxies through the bunker's `nip04`/`nip44` primitives where possible. Roles that *require* a raw private key locally (e.g., running a local c-relay that stores its own nsec) are disabled or limited in this mode.

### 3.3 Per-flavor UX

| Image flavor                       | Mnemonic entry UX                                                    |
|------------------------------------|----------------------------------------------------------------------|
| **amd64 live ISO (text console)**  | Full-screen TUI over `/dev/tty1`, keyboard entry, word auto-complete |
| **amd64 live ISO with GUI**        | Boot-time TUI same as above; the GUI is launched *after* identity is loaded, never before |
| **Raspberry Pi Zero 2 W**          | Waveshare 1.3" LCD HAT with button navigation: wheel to scroll wordlist, press to select; joystick for "next word"; confirmation via button combo. See [`plans/keyboard_gpio_matrix.md`](../../raspberry_pi_zero_nostr/plans/keyboard_gpio_matrix.md). |
| **Headless (SSH) setup**           | Not supported by default. Mnemonic entry MUST be on the physical console to avoid keystroke exfil over the network. A future opt-in "paste via serial" path is possible but out of scope for now. |

## 4. The identity-agent daemon

A single long-running process is the authority for all identity operations. It is the only process that ever sees the mnemonic.

### 4.1 Responsibilities

1. Present the boot-time mnemonic UX (or delegate to a TUI binary it supervises).
2. Hold the mnemonic + derived keys in `mlock()`-ed, non-swappable memory.
3. Derive keys on demand by index.
4. Act as a [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md) signer for apps (local, over Unix socket; optionally over fips mesh for mesh-local apps).
5. Track the role→index schedule; register new roles (§2.3).
6. On shutdown or SIGTERM, zero all key material before exit.

### 4.2 Non-responsibilities

- It does **not** publish events itself. It signs, apps publish.
- It does **not** manage relays. That's app-level.
- It does **not** touch config projection. That's the config-loader/writer (see [`plans/nostr_config_projection.md`](nostr_config_projection.md)). It only *signs* events on behalf of the config-writer.
- It does **not** store the mnemonic anywhere but RAM. No `~/.nostr`, no `/etc/n-os-tr/identity`, nothing.

### 4.3 Processes and isolation

```mermaid
flowchart LR
    User[User at console] -- 12 words --> UX[identity-agent-ui<br/>TUI or LCD]
    UX -- passes via pipe --> Agent[identity-agent<br/>daemon]

    App1[c-relay] -- NIP-46 --> Signer[local signer socket<br/>/run/nostr-id/signer.sock]
    App2[ginxsom] -- NIP-46 --> Signer
    App3[fips] -- NIP-46 --> Signer
    App4[config-loader] -- NIP-46 --> Signer
    Signer --- Agent

    subgraph mem[mlock-ed, non-swap memory]
        Agent
    end
```

The agent runs as its own user (e.g., `nostr-id`), with:
- `MemoryDenyWriteExecute=yes`
- `LockPersonality=yes`
- `NoNewPrivileges=yes`
- `ProtectSystem=strict`
- `ProtectHome=yes`
- `PrivateTmp=yes`
- `RestrictAddressFamilies=AF_UNIX AF_NETLINK`
- `SystemCallArchitectures=native`
- `SystemCallFilter=@system-service`
- `CapabilityBoundingSet=CAP_IPC_LOCK` (for `mlock`), nothing else.

The signer Unix socket is accessible only to a well-defined group (`nostr-signer`). Apps that want to sign add their service user to that group.

### 4.4 Deriving without revealing

Most apps do NOT need the raw private key. They need signed events. For them, the NIP-46 flow is:

```mermaid
sequenceDiagram
    autonumber
    participant App as c-relay
    participant Sig as signer socket
    participant Ag as identity-agent

    App->>Sig: connect(/run/nostr-id/signer.sock)
    App->>Sig: NIP-46 "describe" (role=c-relay-admin)
    Sig->>Ag: resolve role, returns npub
    Ag-->>Sig: npub for role
    Sig-->>App: npub for role

    App->>Sig: NIP-46 "sign_event" (kind=N, tags=..., content=...)
    Sig->>Ag: derive sk for role, sign, zero sk
    Ag-->>Sig: signed event JSON
    Sig-->>App: signed event JSON
```

A small number of apps — those that can't use a remote signer (e.g., legacy tools) — can request the *raw nsec*. That call is gated:
- Only callable by specific whitelisted service users.
- Only for specific role indices.
- Rejected entirely when the user is operating in "external bunker" mode (§3.2 Flow 4).

## 5. Public interface

### 5.1 CLI: `nostr-id`

| Command                          | Description                                                              |
|----------------------------------|--------------------------------------------------------------------------|
| `nostr-id status`                | Show whether the agent is running and how it was bootstrapped (entered / generated / imported / bunker). Never prints keys. |
| `nostr-id show npub <role>`      | Print the npub for a role name. Uses the role→index table from config.   |
| `nostr-id show hex <role>`       | Print the hex pubkey for a role.                                          |
| `nostr-id show fips-ipv6 <role>` | Print the FIPS mesh IPv6 for a role (usually `main`).                     |
| `nostr-id list`                  | Print the role table: name, index, npub.                                  |
| `nostr-id register <service>`    | Allocate a new role index for a service. Interactive confirmation.        |
| `nostr-id derive <role>`         | Print the raw nsec for a role. Gated (see §4.4); prompts for confirmation.|
| `nostr-id sign <file>`           | Sign an unsigned event JSON from stdin/file for a role, print signed.     |
| `nostr-id lock`                  | Zero the mnemonic from RAM *without* shutting down (panic button).        |
| `nostr-id bunker-connect <url>`  | Connect to an external NIP-46 bunker instead of a local mnemonic.         |

The agent also exposes a DBus interface on the system bus under `org.n_os_tr.IdentityAgent` with matching methods, for GUI clients and other language bindings.

### 5.2 Library: `libnostr-id` (planned)

A small C library + Rust crate + Python binding that wraps the signer socket. Apps depend on this instead of implementing NIP-46 themselves.

### 5.3 Integration with current services

After Phase 3 lands, the services that exist today ([`fips`](../includes/fips/), [`c-relay`](../includes/c-relay/), [`ginxsom`](../includes/ginxsom/)) will drop their current "generate a fresh key on first boot" behavior and instead:

- On startup, call `nostr-id show npub <role>` + `nostr-id sign ...` (or `nostr-id derive <role>` for services that require raw keys).
- Their systemd units add `After=nostr-id.service` and `Requires=nostr-id.service`.
- The [`n-os-tr-firstboot`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-firstboot) script becomes a much thinner shim: identity generation moves out of it entirely, into the agent.

## 6. Memory safety mechanics

Enforcing [F1–F7 of the threat model](threat_model.md#41-the-mnemonic-and-derived-keys) requires deliberate engineering:

- **`mlock(2)`.** The agent locks pages containing mnemonic and derived keys so the kernel can't swap them. Requires `CAP_IPC_LOCK`.
- **Zeroize-on-drop.** Key material is wrapped in a type (Rust `zeroize::Zeroizing`, or C `volatile` + explicit_bzero) that guarantees the bytes are cleared when the owner is dropped.
- **No `core` dumps.** `prctl(PR_SET_DUMPABLE, 0)` on the agent process.
- **No `ptrace`.** `prctl(PR_SET_DUMPABLE, 0)` blocks `ptrace` too; additionally the agent's systemd unit sets `NoNewPrivileges=yes` and restricts `SYS_PTRACE` via seccomp.
- **No `/proc/pid/mem`.** Disabled by `PR_SET_DUMPABLE=0`.
- **No swap, ever.** Swap is disabled at the kernel level at image build time (see [F2](threat_model.md#41-the-mnemonic-and-derived-keys)).
- **Shutdown handler.** On SIGTERM the agent explicitly zeros every buffer, then exits. If the OS is going down, it does not wait for journald to flush.

## 7. Failure modes and corner cases

### 7.1 Invalid mnemonic entered

- UX rejects the phrase, clears the input buffer, prompts again.
- After N=3 failed attempts on the Pi LCD form factor, optionally offer to generate a new one (since the user clearly does not have their phrase).

### 7.2 User wants to switch identities mid-session

- `nostr-id lock` panic button zeros the current mnemonic.
- The session is now unusable for any signed action; user is prompted to re-enter a mnemonic or shut down.
- All apps connected to the signer socket receive a "signer unavailable" error; they are expected to fail closed.

### 7.3 Agent crashes

- The systemd unit restarts it, but the restart has no mnemonic loaded.
- All apps see "signer unavailable" until the user re-enters the mnemonic.
- The session is effectively over. We do not attempt to persist "oh just re-load from somewhere" — the mnemonic is user-only input.

### 7.4 Clock skew at first boot

- Nostr signatures don't depend on the wall clock, but relay event timestamps do.
- Identity-agent does not care about clock. Event-emitting apps do. Addressed separately by network-bootstrap via NTP.

### 7.5 User generates a new identity but never writes it down

- This is a user problem. The UX should make non-confirmation *very* obvious (the 3-random-word-position confirmation step in §3.2 Flow 2).

### 7.6 External NIP-46 bunker goes offline mid-session

- Signing fails. Apps see signer errors. If the bunker comes back, resume.
- If the bunker is permanently gone, the session is effectively read-only and the user must shut down.

## 8. Implementation plan (Phase 3a)

Each step is independently testable and each leans on an existing `nostr_core_lib` primitive rather than reimplementing crypto.

1. **Specify schedule.** Lock the index table in §2.1 behind a version number. Ship it inside a shared constants file (`stack/identity/schedule.toml` or equivalent) so all apps reference the same source of truth.

2. **Build `libnostr_core.a` under Alpine/musl.** Every identity-related binary is built following the same Alpine+musl discipline [`nostr_core_lib`](../includes/nostr_core_lib/), [`includes/ginxsom`](../includes/ginxsom/), and [`includes/c-relay`](../includes/c-relay/) already use — see [`plans/tui_login.md §0.1`](tui_login.md#01-toolchain-alpine--musl-dev-built-in-docker) for the full toolchain recipe. We reuse the shared `stack/build-common/Dockerfile.alpine-musl` landed in TUI step 1. No glibc, no `.deb`; the library is linked statically into each consumer binary produced from this builder.

3. **Prototype CLI `nostr-id`.** Single fully static C99 binary (`nostr-id_static_x86_64`) produced by the Alpine/musl builder, linking [`libnostr_core.a`](../includes/nostr_core_lib/). Reads mnemonic from stdin, calls [`nostr_derive_keys_from_mnemonic`](../includes/nostr_core_lib/nostr_core/nip006.h:24) for the requested role index, prints the npub via [`nostr_key_to_bech32`](../includes/nostr_core_lib/nostr_core/nostr_core.h). No daemon yet; no IPC; pure unit-testable against [`includes/nostr_core_lib/tests`](../includes/nostr_core_lib/tests/). `ldd` on the resulting binary must report "not a dynamic executable."

4. **Wrap as daemon.** Turn the CLI into a long-running agent:
   - Holds mnemonic + all derived keys in `mlock()`-ed memory.
   - Exposes a Unix-domain control socket at `/run/nostr-id/control.sock` for CLI commands.
   - Runs the NIP-46 signer loop via [`nostr_nip46_signer_session_init`](../includes/nostr_core_lib/nostr_core/nip046.h:163) + [`nostr_nip46_signer_handle_request`](../includes/nostr_core_lib/nostr_core/nip046.h:169), receiving requests over a local Unix socket (or over a loopback relay for mesh-capable apps).
   - Wires [`nostr_set_log_callback`](../includes/nostr_core_lib/nostr_core/nostr_log.h) to route library logs to journald (no on-disk log files, per [F10–F11 in threat_model.md](threat_model.md#42-on-disk-state)).
   - Handles SIGTERM by zeroing keys and exiting cleanly.

5. **Write TUI front end.** Curses-style boot-time entry for amd64 TTY. Accepts word-by-word mnemonic input, validates each word against the BIP-39 list, validates the checksum via [`nostr_derive_keys_from_mnemonic`](../includes/nostr_core_lib/nostr_core/nip006.h:24) (return code surfaces an invalid mnemonic). Generation flow uses [`nostr_generate_mnemonic_and_keys`](../includes/nostr_core_lib/nostr_core/nip006.h:21). External-signer flow uses [`nostr_nip46_client_session_init`](../includes/nostr_core_lib/nostr_core/nip046.h:145) with the user-supplied bunker URL.

6. **Retrofit Phase 2 services.**
   - c-relay: switch [`iso/config/includes.chroot/etc/systemd/system/c-relay.service`](../iso/config/includes.chroot/etc/systemd/system/c-relay.service) to pull admin pubkey via `nostr-id show npub c-relay-admin` and admin privkey via `nostr-id derive c-relay-admin` (raw-key path, gated).
   - ginxsom: same pattern, role `ginxsom` (NIP-06 index 3).
   - fips: same pattern, role `fips` (NIP-06 index 4). Also receives its FIPS IPv6 from the agent.
   - [`iso/config/includes.chroot/usr/local/sbin/n-os-tr-firstboot`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-firstboot) becomes much thinner — identity generation moves to the agent entirely.

7. **Smoke tests.** Extend [`iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-smoketest) to verify:
   - Agent is running and the control socket is present.
   - Given a fixed test mnemonic (`test vector v1`), the 5 role npubs match hard-coded expected values — this guards against accidental changes to derivation semantics and is easy to cross-check against `nostr_core_lib`'s own tests.
   - No `nsec1`, no 64-char lowercase hex that looks like a privkey, appears in `journalctl` output.
   - NIP-46 ping round-trips over the signer socket via [`nostr_nip46_client_ping`](../includes/nostr_core_lib/nostr_core/nip046.h:156).

8. **LCD front end for Pi.** Comes later, as part of the Pi-Zero-2-W image flavor. Consumes the same control socket as the amd64 TUI.

9. **Runtime self-test.** On boot, identity-agent runs a quick self-test (derive a known-vector mnemonic → compare output to embedded expected npub) before declaring itself ready. If the library is miscompiled or `libsecp256k1` is missing, this fails fast with a visible error instead of silently producing wrong keys.

## 9. Open questions

- **Mnemonic strength.** 12 words = 128 bits of entropy. Is this enough? BIP-39 also supports 15/18/21/24-word phrases for 160/192/224/256 bits. n-OS-tr defaults to 12 for UX; 24 could be opt-in. Decision: default 12, allow longer phrases without breaking derivation.
- **Per-relay ephemeral keys.** Do we want a fresh throwaway key *per relay connection* for metadata hygiene? That's beyond NIP-06 indexing; probably an app-level concern (client rotates its own subkeys off index 1 via BIP-32 sub-derivation). Revisit in a later doc.
- **Hardware wallets.** `nostr-id bunker-connect` is a generic path for external signers, but hardware-wallet-specific UX (pair, unlock, confirm-on-device) is richer. Defer to Phase 4.
- **Identity handover.** Is there a safe way to pass a live session from one n-OS-tr machine to another without the user re-typing the mnemonic? E.g., QR-code export from one machine to another over a camera. Non-goal for v1; note for later.
- **Amnesia mode.** Some users may want to boot *without* a mnemonic at all (guest session, hostile environment). That means generating a key that exists only for this boot and is never written anywhere. Flow is: `nostr-id amnesia` — create ephemeral main key, skip config projection, live session only. Worth shipping in v1.
