# Integrating `n_signer` into n-OS-tr > **Status:** Design (revised for `n_signer` v0.0.16 `--preapprove` model). Defines how the vendored [`includes/n_signer/`](../includes/n_signer/) program becomes the boot-time mnemonic-entry + signer for n-OS-tr, replacing the previously-planned `identity-agent` daemon, and how the user gets a working multi-terminal experience without going GUI. > > Supersedes the daemon design in [`plans/identity_subsystem.md`](identity_subsystem.md). The mnemonic-entry portion of [`plans/tui_login.md`](tui_login.md) is also superseded; the **post-unlock service menu** part of that doc remains valid (now hosted by [`stack/nostr-id-tui/`](../stack/nostr-id-tui/) on tty2). > > Companion docs: [`plans/N_OS_TR_AGENT_CHANGES.md`](N_OS_TR_AGENT_CHANGES.md) (operational guide for the v0.0.16 approval model), [`plans/binary_handling_strategy.md`](binary_handling_strategy.md) (how all binaries get into the ISO), [`plans/upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md) (open upstream features still needed). ## 1. Decisions locked in 1. **`n_signer` replaces the planned `identity-agent`.** `nsigner` already provides everything the design called for: mlock'd RAM-only key material, NIP-06 role derivation, NIP-46-style JSON-RPC signing methods, abstract-namespace Unix socket transport, zero-on-exit. We are not building a parallel daemon; we **delete** that work item. 2. **TTY1 is the signer terminal.** `nsigner` runs as a foreground program owning `/dev/tty1`. Mnemonic entry, status display, and approval prompts all live there. 3. **TTY2 is the post-unlock service-selection menu.** Trimmed [`stack/nostr-id-tui/`](../stack/nostr-id-tui/) on tty2 lets the user toggle service daemons. 4. **TTY3..TTY6 are normal login shells.** User logs in as the `nostr` user, runs `nt`, `otp`, and any other CLIs from those shells. 5. **No GUI in v1.** Virtual consoles + Ctrl+Alt+Fn give multi-terminal for free. 6. **`n_signer` v0.0.16+ `--preapprove` is the policy mechanism.** No separate policy file, no `--role-schedule` flag — preapproved caller/role pairs are passed as repeatable CLI flags on the `nsigner` ExecStart line. 7. **`@n_os_tr`** is the pinned abstract socket name. All consumers hard-code it. 8. **Service daemons get raw nsec at startup; user CLIs sign via NIP-46.** Two patterns coexist (see §7). ## 2. The components in scope This integration covers **eight components** from the n-OS-tr ecosystem. They split into three categories by lifecycle: | Category | Components | What they need from nsigner | |---|---|---| | **The signer itself** | [`n_signer`](../includes/n_signer/) | Holds the mnemonic; serves the rest | | **Service daemons** (long-running, started by service menu) | [`c-relay`](../includes/c-relay/), [`ginxsom`](../includes/ginxsom/), [`fips`](../includes/fips/), `didactyl` | Raw nsec at startup (NIP-44 ECDH, Noise handshakes) | | **User CLIs** (launched by user from shell) | `nt` (nostr_terminal), `otp`, future tools | NIP-46 `sign_event` over `@n_os_tr` | | **Boot-time TUIs** (n-OS-tr-native) | [`nostr-id-tui`](../stack/nostr-id-tui/) (services menu) | NIP-46 `get_public_key(nostr_index=0)` to display the user's npub | The role index assignment ([`plans/identity_subsystem.md` §2.1](identity_subsystem.md#21-the-role-table-schedule)) updated: | Index | Role | Used by | Category | |---|---|---|---| | `0` | `main` | User's primary identity (`nt`, `otp`, future user tools) | User | | `1` | `throwaway` | Test/experimental | User | | `2` | `c-relay-admin` | c-relay daemon | Service | | `3` | `ginxsom` | ginxsom daemon | Service | | `4` | `fips` | fips mesh daemon | Service | | `5` | `didactyl` | didactyl agent daemon | Service | | `6+` | reserved/dynamic | future apps via registration | — | ## 3. Boot-time UX ```mermaid sequenceDiagram participant Kernel participant Sd as systemd participant Fb as n-os-tr-firstboot participant TTY1 as nsigner @ tty1 participant TTY2 as nostr-id-tui @ tty2 participant TTY3 as getty @ tty3..6 participant Daemon as c-relay / ginxsom / fips / didactyl participant CLI as nt / otp on user shell Kernel->>Sd: hand off Sd->>Fb: oneshot (no key generation) Fb-->>Sd: done Sd->>TTY1: start nsigner.service
with --preapprove flags Sd->>TTY2: start nostr-id-tui-services.service Sd->>TTY3: getty units (lazy-start on switch) Note over TTY1: User sees: Enter mnemonic /
Generate new Note over TTY2: User sees: Waiting for signer
polling @n_os_tr TTY1->>TTY1: user types mnemonic TTY1->>TTY1: derive role keys, bind @n_os_tr
load preapprove rules TTY2->>TTY1: get_public_key nostr_index=0
(preapproved for uid:0) TTY1-->>TTY2: npub1xyz... TTY2->>TTY2: show service menu TTY2->>Daemon: systemctl start Daemon->>TTY1: get_secret_key nostr_index=N
(preapproved for the daemon's uid) TTY1-->>Daemon: hex privkey Daemon->>Daemon: hold key in mlock'd RAM,
do its job Note over TTY3: user logs in on tty3 Note over CLI: user runs nt or otp CLI->>TTY1: sign_event nostr_index=0
(preapproved for uid:1000) TTY1-->>CLI: signed event ``` ### 3.1 Concrete sequence 1. Boot completes. tty1 shows the `nsigner` mnemonic prompt. tty2 shows "waiting for signer" and starts polling `@n_os_tr`. tty3..tty6 are dormant. 2. User stays on tty1, types or generates the 12-word mnemonic. `nsigner` derives all role keys (indices 0–5 are pre-registered via the `--preapprove ...,nostr_index=N` startup flags), binds `@n_os_tr`, and the preapprove table is now active. 3. tty2's polling loop's first `get_public_key(nostr_index=0)` request hits — preapproved for `uid:0` (root) — returns the npub immediately, no prompt on tty1. 4. tty2 displays the npub for confirmation, then enters the service menu. 5. User toggles services on. `nostr-id-tui` does `systemctl start c-relay.service`. c-relay's `ExecStartPre` calls `n-os-tr-get-key c-relay-admin` which talks to `@n_os_tr`, asks for `get_secret_key(nostr_index=2)` — preapproved for the c-relay system user's uid — and exports the hex key into the service's environment. 6. Daemon runs. Same pattern for ginxsom, fips, didactyl. 7. User Ctrl+Alt+F3, logs in as `nostr` (uid 1000), runs `nt`. `nt` connects to `@n_os_tr`, calls `sign_event(nostr_index=0)`. Preapproved for `uid:1000`. Event is signed, no prompt on tty1. ### 3.2 Knowing which TTY you're on (visual indicators) Linux gives you 6 virtual consoles for free, but stock Debian gives **no obvious indicator** of which one you're currently looking at. We address this with two layers, both essentially free: **Layer 1 — Distinct banner per TTY.** | TTY | Banner contents | Owner | |---|---|---| | tty1 | `🔐 N-OS-TR SIGNER · TTY1` in red, full-width | `nsigner` (via wrapper or upstream `--banner-prefix`) | | tty2 | `⚙️ N-OS-TR SERVICES · TTY2` in blue, full-width | trimmed `nostr-id-tui` | | tty3..tty6 | `n-OS-tr · SHELL · tty\l` baked into [`/etc/issue`](../iso/config/includes.chroot/etc/issue); shell `PS1=[tty\l \w]\$ ` | getty + `/etc/profile.d/n-os-tr-tty.sh` | **Layer 2 — Background color tint per TTY.** Set via `setterm` in each unit's `ExecStartPre`. | TTY | Tint | |---|---| | tty1 | dark red | | tty2 | dark blue | | tty3..tty6 | default | The combination — color + banner + shell prompt — means at a glance the user can tell signer (red) from service menu (blue) from shell (black with `[ttyN]` prompt). Color-blind-friendly: shapes (🔐 vs ⚙️) and prompts also differ. > **Open question (T9 in [`upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md)).** Adding "N-OS-TR SIGNER · TTY1" to nsigner's hardcoded banner needs upstream `--banner-prefix STRING` flag, OR a wrapper script, OR a build-time source patch. Lean: upstream. ### 3.3 Recovery / unhappy-path UX - **Bad mnemonic.** `nsigner` clears its buffer and reprompts on tty1. tty2 keeps polling. - **User quits `nsigner` (`q` hotkey).** Process exits, abstract socket disappears, key material is gone. systemd `Restart=always` brings up a fresh `nsigner` and the cycle starts over. tty2's polling loop catches the new instance; if the npub differs (different mnemonic), tty2 re-confirms before re-entering service menu. - **A daemon crashes.** `nsigner` is unaffected. systemd's restart policy handles the daemon; on restart it re-fetches its key via `n-os-tr-get-key`. - **A user CLI gets denied.** If the user runs `nt` from a uid not covered by a `--preapprove`, the first `sign_event` call returns `policy_denied`. The CLI surfaces "approval needed; check tty1" and the user walks to tty1, sees the prompt, presses `[a]` for session-grant. ## 4. nsigner.service — the core configuration The service unit on tty1 that orchestrates everything. Replaces the existing [`nostr-id-tui.service`](../iso/config/includes.chroot/etc/systemd/system/nostr-id-tui.service). ```ini [Unit] Description=n-OS-tr signer (n_signer foreground) Documentation=file:///usr/share/doc/n-os-tr/README After=systemd-user-sessions.service n-os-tr-firstboot.service Wants=n-os-tr-firstboot.service Conflicts=getty@tty1.service Before=getty@tty1.service [Service] Type=simple ExecStartPre=/usr/bin/setterm -term linux -background red -store ExecStart=/usr/local/sbin/nsigner \ --socket-name n_os_tr \ \ # tty2 service-menu polls for the user's npub. Root uid. --preapprove caller=uid:0,nostr_index=0 \ \ # User CLIs (nt, otp) signing under the user identity. # The 'nostr' user is uid 1000 in the live image. --preapprove caller=uid:1000,nostr_index=0 \ \ # Service daemons. Each runs as its own dedicated system user. --preapprove caller=uid:%U_C_RELAY%,nostr_index=2 \ --preapprove caller=uid:%U_GINXSOM%,nostr_index=3 \ --preapprove caller=uid:%U_FIPS%,nostr_index=4 \ --preapprove caller=uid:%U_DIDACTYL%,nostr_index=5 Restart=always RestartSec=1 StandardInput=tty StandardOutput=tty StandardError=journal TTYPath=/dev/tty1 TTYReset=yes TTYVHangup=yes TTYVTDisallocate=yes # Memory hygiene (mirrors what n_signer enforces in-process) LockPersonality=yes NoNewPrivileges=yes MemoryDenyWriteExecute=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes RestrictAddressFamilies=AF_UNIX SystemCallArchitectures=native CapabilityBoundingSet=CAP_IPC_LOCK AmbientCapabilities=CAP_IPC_LOCK [Install] WantedBy=multi-user.target ``` **About the `%U_*%` placeholders.** systemd doesn't expand variables inside `ExecStart`. We have two options: - **Option 4a (recommended): generate the unit at first boot** with the actual uids, after the system users are created. A small wrapper script reads `/etc/passwd`, looks up `c-relay`, `ginxsom`, etc., and writes the final unit file. Done once, at firstboot. - **Option 4b:** Allocate fixed system uids in [`iso/config/hooks/live/0020-enable-services.hook.chroot`](../iso/config/hooks/live/0020-enable-services.hook.chroot) (e.g., `c-relay` is always uid 901, `ginxsom` always 902, etc.) and hard-code those in the unit. Simpler but less flexible. Lean: **Option 4a**. The wrapper lives at [`iso/config/includes.chroot/usr/local/sbin/n-os-tr-render-nsigner-unit`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-render-nsigner-unit) and runs as part of `n-os-tr-firstboot`. ### 4.1 Why every preapprove uses `nostr_index=N` We use `nostr_index=N` (not `role=`) for every preapprove rule. Reasons: 1. **Indices are canonical.** They're the actual NIP-06 derivation parameter; role names are just labels. 2. **No role-schedule file needed.** v0.0.16's `--preapprove ...,nostr_index=N` auto-creates the role at parse time. We get role registration for free as a side effect of the preapprove rules. **This closes upstream ticket T1.** 3. **The role-name list lives in [`plans/identity_subsystem.md` §2.1](identity_subsystem.md#21-the-role-table-schedule)** as the human-readable mapping. Code uses indices. The auto-created role names from `nostr_index=N` are: - index 0 → role `main` - index N>0 → role `nostr_idx_N` (e.g., `nostr_idx_2`, `nostr_idx_5`) So when c-relay calls `n-os-tr-get-key 2`, the helper translates that to `{"nostr_index": 2}` in the wire request, the response carries the key. The "c-relay-admin" name from our role schedule is purely documentation; it doesn't appear on the wire. ## 5. The key-fetch helper for service daemons [`/usr/local/bin/n-os-tr-get-key`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-get-key) is the bridge between v0.0.16's RPC and a daemon's `ExecStartPre`. It already exists in the repo. Updated for v0.0.16 wire contract: ```bash #!/bin/bash # n-os-tr-get-key: request a role's secret key from nsigner via @n_os_tr # Usage: n-os-tr-get-key # Outputs: 64-char hex private key on stdout. # Exit 1 on failure (signer down, denied, role not preapproved, ...). set -euo pipefail INDEX="${1:?usage: n-os-tr-get-key }" SOCKET="n_os_tr" TIMEOUT="${NSIGNER_KEY_TIMEOUT:-30}" # Wait for socket. If user never unlocks signer, this times out and we fail closed. ELAPSED=0 while [ "$ELAPSED" -lt "$TIMEOUT" ]; do if socat -u OPEN:/dev/null ABSTRACT-CONNECT:"$SOCKET" 2>/dev/null; then break fi sleep 1 ELAPSED=$((ELAPSED + 1)) done [ "$ELAPSED" -ge "$TIMEOUT" ] && { echo "n-os-tr-get-key: timeout waiting for @${SOCKET}" >&2; exit 1; } # Build framed request (4-byte big-endian length + JSON payload) PAYLOAD='{"id":"1","method":"get_secret_key","params":[{"nostr_index":'"$INDEX"'}]}' PAYLOAD_LEN=${#PAYLOAD} FRAME=$(printf '%08x' "$PAYLOAD_LEN" | xxd -r -p; printf '%s' "$PAYLOAD") RESPONSE=$(printf '%s' "$FRAME" | socat -t5 - ABSTRACT-CONNECT:"$SOCKET" | tail -c +5) # Check for error response if echo "$RESPONSE" | grep -q '"error"'; then ERR=$(echo "$RESPONSE" | grep -o '"message":"[^"]*"' | cut -d'"' -f4) echo "n-os-tr-get-key: signer error: ${ERR:-unknown}" >&2; exit 1 fi # Extract 64-char hex from result field KEY=$(echo "$RESPONSE" | grep -oE '"result":"[a-f0-9]{64}"' | grep -oE '[a-f0-9]{64}' | head -1) [ -z "$KEY" ] || [ "${#KEY}" -ne 64 ] && { echo "n-os-tr-get-key: invalid response" >&2; exit 1; } echo "$KEY" ``` > **Critical caveat.** `get_secret_key` is **not yet implemented in upstream `n_signer`** as of v0.0.16. This is open ticket T3 in [`plans/upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md). Until it lands, the helper above will receive `method_not_found` and the daemons will fail closed. See §9 for the path forward. ## 6. Per-daemon systemd drop-ins Each service daemon gets a drop-in that adds the signer-aware behavior. These already exist for c-relay, ginxsom, fips, nginx; we add didactyl. ### 6.1 c-relay [`/etc/systemd/system/c-relay.service.d/n-os-tr-signer.conf`](../iso/config/includes.chroot/etc/systemd/system/c-relay.service.d/n-os-tr-signer.conf): ```ini [Unit] After=nsigner.service Wants=nsigner.service [Service] Environment=NSIGNER_SOCKET=@n_os_tr ExecStartPre=/usr/local/bin/n-os-tr-await-signer # Get the relay-admin key, export to env. The key never touches disk. ExecStartPre=/bin/bash -c 'echo "C_RELAY_ADMIN_PRIVKEY=$(/usr/local/bin/n-os-tr-get-key 2)" > /run/c-relay.env && chmod 0400 /run/c-relay.env' EnvironmentFile=/run/c-relay.env ExecStartPost=/bin/rm -f /run/c-relay.env ``` The transient `/run/c-relay.env` exists for milliseconds between `ExecStartPre` and `ExecStartPost`. systemd reads `EnvironmentFile=` between the two. ### 6.2 ginxsom Same pattern, `nostr_index=3`. Ginxsom takes `--server-privkey` as a CLI arg, so the drop-in also rewrites the `ExecStart`: [`/etc/systemd/system/ginxsom.service.d/n-os-tr-signer.conf`](../iso/config/includes.chroot/etc/systemd/system/ginxsom.service.d/n-os-tr-signer.conf): ```ini [Unit] After=nsigner.service Wants=nsigner.service [Service] Environment=NSIGNER_SOCKET=@n_os_tr ExecStartPre=/usr/local/bin/n-os-tr-await-signer ExecStartPre=/bin/bash -c 'echo "GINXSOM_SERVER_PRIVKEY=$(/usr/local/bin/n-os-tr-get-key 3)" > /run/ginxsom.env && chmod 0400 /run/ginxsom.env' EnvironmentFile=/run/ginxsom.env # Inherit the upstream ExecStart but add the privkey as an env-substituted arg ExecStart= ExecStart=/usr/local/bin/ginxsom/ginxsom-fcgi --server-privkey ${GINXSOM_SERVER_PRIVKEY} ExecStartPost=/bin/rm -f /run/ginxsom.env ``` ### 6.3 fips `nostr_index=4`. fips reads its identity from a config field. We render the YAML at startup: [`/etc/systemd/system/fips.service.d/n-os-tr-signer.conf`](../iso/config/includes.chroot/etc/systemd/system/fips.service.d/n-os-tr-signer.conf): ```ini [Unit] After=nsigner.service Wants=nsigner.service [Service] Environment=NSIGNER_SOCKET=@n_os_tr ExecStartPre=/usr/local/bin/n-os-tr-await-signer # Render fips config with the live nsec from nsigner. /run is tmpfs. ExecStartPre=/bin/bash -c '\ KEY=$(/usr/local/bin/n-os-tr-get-key 4) && \ install -d -m 0700 /run/fips && \ cp /etc/fips/fips.yaml /run/fips/fips.yaml && \ echo "node:" >> /run/fips/fips.yaml && \ echo " identity:" >> /run/fips/fips.yaml && \ echo " nsec: $KEY" >> /run/fips/fips.yaml && \ chmod 0400 /run/fips/fips.yaml' ExecStart= ExecStart=/usr/local/bin/fips --config /run/fips/fips.yaml ``` ### 6.4 didactyl (new) `nostr_index=5`. Didactyl reads its key from a `genesis.jsonc` config file: [`/etc/systemd/system/didactyl.service`](../iso/config/includes.chroot/etc/systemd/system/didactyl.service): ```ini [Unit] Description=Didactyl Nostr agent After=nsigner.service network-online.target Wants=nsigner.service network-online.target [Service] Type=simple User=didactyl Group=didactyl Environment=NSIGNER_SOCKET=@n_os_tr ExecStartPre=/usr/local/bin/n-os-tr-await-signer ExecStartPre=/bin/bash -c '\ KEY_HEX=$(/usr/local/bin/n-os-tr-get-key 5) && \ install -d -m 0700 -o didactyl -g didactyl /run/didactyl && \ /usr/local/bin/n-os-tr-render-didactyl-genesis "$KEY_HEX" > /run/didactyl/genesis.jsonc && \ chmod 0400 /run/didactyl/genesis.jsonc && \ chown didactyl:didactyl /run/didactyl/genesis.jsonc' ExecStart=/usr/local/bin/didactyl_static_x86_64 --config /run/didactyl/genesis.jsonc Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target ``` The render helper [`n-os-tr-render-didactyl-genesis`](../iso/config/includes.chroot/usr/local/bin/n-os-tr-render-didactyl-genesis) takes the hex key, derives npub/nsec from it (using `nak` or a small helper), and emits the JSON didactyl expects (see [`didactyl/README.md`](../../didactyl/README.md) for schema). ### 6.5 nginx No change from current — already gated by `n-os-tr-await-signer` so it doesn't start before identity is loaded. ## 7. User CLIs: `nt`, `otp`, future tools User CLIs are different from service daemons: - They're launched by the **user**, from a shell, on tty3+ - They run as the user's uid (e.g., `uid:1000` for the `nostr` user) - They want to sign Nostr events, not run forever - They should NOT need raw nsec — they should use NIP-46 `sign_event` cleanly ### 7.1 Configuration: tell each CLI where the signer is Both `nt` and `otp` (and future user CLIs) need a way to discover the signer. We standardize on: - **Environment variable** `NOSTR_SIGNER_SOCKET=@n_os_tr` exported in the user's shell (set in [`/etc/profile.d/n-os-tr-tty.sh`](../iso/config/includes.chroot/etc/profile.d/n-os-tr-tty.sh)). - **Login flow:** when `nt` starts and finds `NOSTR_SIGNER_SOCKET` set, it skips its built-in mnemonic prompt and uses the signer instead. (This is a per-CLI feature that needs implementing — see §9.) ### 7.2 Preapprove for the user's uid The `nsigner.service` unit's preapprove flags include `caller=uid:1000,nostr_index=0`, granting the user (uid 1000) auto-allow access to sign_event for role `main`. So: ```bash # On tty3, logged in as nostr user: $ nt [nt] using signer @n_os_tr (uid:1000 → nostr_index=0) [nt] hello, npub1xyz... > publish "hello world" [nt] signed via nsigner, published to relays ``` No prompt fires on tty1; the request is preapproved. If the user wants more granular control (e.g., always-prompt for `otp` because the messages are sensitive), they can either: - Remove the `caller=uid:1000,nostr_index=0` preapprove flag from `nsigner.service` (every CLI signing requires tty1 approval), or - Run with a different uid for the sensitive CLI (run `otp` as `uid:1001`, no preapprove for that uid) ### 7.3 What needs to change in `nt` and `otp` Both currently expect the user to enter a mnemonic/nsec in their own UI. To use n_signer instead, each CLI needs to: 1. Detect `NOSTR_SIGNER_SOCKET` env var and a `--use-signer` flag (or treat env-var presence as opt-in) 2. Implement a NIP-46 over abstract-Unix-socket client (the same JSON-RPC framing nsigner uses) 3. Replace local mnemonic/nsec storage with calls to `get_public_key` and `sign_event` 4. Fall back to local mnemonic if signer isn't present (preserves existing behavior outside n-OS-tr) This is **upstream work in `nt` and `otp`**, not in this repo. We file tickets against those projects: > **Open question (T11).** File feature requests in `nostr_terminal` and `otp` for "signer integration" — accept `NOSTR_SIGNER_SOCKET` env var, speak nsigner's framed JSON-RPC, fall back gracefully if absent. Track outside this plan. ## 8. The n-os-tr service menu (tty2) [`stack/nostr-id-tui/`](../stack/nostr-id-tui/) trimmed down to just the post-unlock service menu. Polls `@n_os_tr` for the main npub, then lets the user toggle service daemons on/off. The service list (in [`/etc/n-os-tr/services.conf`](../iso/config/includes.chroot/etc/n-os-tr/services.conf)) now includes `didactyl`: ``` # /etc/n-os-tr/services.conf — services toggleable from tty2 c-relay:c-relay.service:Nostr relay (your own NIP-01 relay) ginxsom:ginxsom.service:Blossom blob server (BUD-* protocols) fips:fips.service:FIPS mesh routing (npub-addressed IPv6) didactyl:didactyl.service:Didactyl AI agent nginx:nginx.service:HTTPS / reverse proxy ``` (Format: `display-name:unit-name:description`.) ## 9. Required upstream work To make the integration fully functional we still need the following, tracked in [`plans/upstream_nsigner_tickets.md`](upstream_nsigner_tickets.md): | Ticket | Status | What | Blocks | |---|---|---|---| | T1: `--role-schedule FILE` | **Closed** | Resolved by `--preapprove ...,nostr_index=N` auto-creating roles | nothing | | T2: `--policy-file PATH` | **Closed** | Resolved by `--preapprove` CLI flag | nothing | | T3: `get_secret_key` method | **Open** | RPC method to expose raw nsec for a role index, gated by policy | All four service daemons | | T9: `--banner-prefix STRING` | **Open**, low-priority | Inject "N-OS-TR SIGNER · TTY1" into nsigner banner | UX nice-to-have | | T11: `nt` and `otp` signer integration | **Open** | Upstream changes in those projects to use NIP-46 over `@n_os_tr` | User CLI signing | T3 is the critical-path blocker. Until it lands, we can ship the ISO with: - nsigner working (mnemonic entry, status display, prompts) - tty2 service menu working (uses `get_public_key` which exists) - User CLIs working **if `nt`/`otp` are updated** to use `sign_event` (T11) - Service daemons **broken** — they can't get raw keys Mitigation while T3 is pending: keep [`n-os-tr-firstboot`](../iso/config/includes.chroot/usr/local/sbin/n-os-tr-firstboot)'s old key-generation path as a feature-flag fallback. If `n-os-tr-get-key` returns `method_not_found`, the daemon's `ExecStartPre` falls back to the old behavior (random nsec generated at firstboot, stored in `/var/lib/n-os-tr/keys/`). This is **insecure** by our threat model — the keys touch disk — but it lets the rest of the system work for testing while we land T3 upstream. ## 10. Threat-model implications Strengthens [`plans/threat_model.md`](threat_model.md): - **F1 (no on-disk mnemonic):** preserved. nsigner mlocks; mnemonic never persisted. - **F2 (no swap):** preserved. nsigner calls mlock; image disables swap kernel-wide. - **F4 (no key material in journald):** preserved. nsigner doesn't log keys; the transient `/run/.env` files are short-lived tmpfs. - **F12 (no detached daemon for keys):** preserved. nsigner is foreground, attached to tty1, dies cleanly. New attack surfaces: - **The `@n_os_tr` socket itself.** Gated by `SO_PEERCRED`; preapprove rules pin specific uids. Anyone running outside the preapprove list still hits the prompt path on tty1. - **The `--preapprove` CLI args.** Visible in `ps`/`/proc`. Contents are public (uid + role index, no secrets), so this is benign. - **`/run/.env` transient files.** Exist for milliseconds during ExecStartPre→ExecStart→ExecStartPost. On tmpfs (RAM, no swap). Mode 0400. Cleared by ExecStartPost. Acceptable. ## 11. Documents that need updating after this lands - [`plans/identity_subsystem.md`](identity_subsystem.md) — already has deprecation banner. Update §2.1 to add didactyl at index 5. - [`plans/iso_architecture.md`](iso_architecture.md) — boot diagram updated to show didactyl alongside the other daemons. - [`plans/binary_handling_strategy.md`](binary_handling_strategy.md) — add didactyl, nostr_terminal, otp to the Tier 1 component list. - [`plans/N_OS_TR_AGENT_CHANGES.md`](N_OS_TR_AGENT_CHANGES.md) — already up-to-date for v0.0.16; this plan integrates with it. - [`README.md`](../README.md) — add `nt`, `otp`, `didactyl` to the components list; mention multi-VT briefly. - [`docs/RUNNING.md`](../docs/RUNNING.md) — already has VT switching section; add CLI usage examples for `nt` and `otp`. ## 12. Summary diagram ```mermaid flowchart TB subgraph TTYs[Linux virtual consoles] T1[tty1 RED: nsigner
mnemonic + status + prompts] T2[tty2 BLUE: nostr-id-tui
service menu] T3[tty3..tty6: shell
uid:1000 nostr user] end Sock[@n_os_tr
abstract Unix socket, pinned name
--preapprove rules loaded at startup] subgraph Daemons[Service daemons - long-running] Cr[c-relay
uid:c-relay
nostr_index=2] Gx[ginxsom
uid:ginxsom
nostr_index=3] Fp[fips
uid:fips
nostr_index=4] Dd[didactyl
uid:didactyl
nostr_index=5] end subgraph CLIs[User CLIs - launched from shell] Nt[nt
nostr terminal] Otp[otp
one-time pad] end T1 -- binds --> Sock T2 -- get_public_key nostr_index=0
caller=uid:0 preapproved --> Sock Cr -- get_secret_key nostr_index=2
caller=uid:c-relay preapproved --> Sock Gx -- get_secret_key nostr_index=3 --> Sock Fp -- get_secret_key nostr_index=4 --> Sock Dd -- get_secret_key nostr_index=5 --> Sock Nt -- sign_event nostr_index=0
caller=uid:1000 preapproved --> Sock Otp -- sign_event nostr_index=0 --> Sock Sock -.prompt for unknown callers.-> T1 ```