# Signer Providers Didactyl can sign Nostr events and perform NIP-04/NIP-44 encryption either with an in-process private key (`local`) or by delegating to a running [`n_signer`](../n_signer) process over one of five transports. In any `nsigner_*` mode the agent process holds **no nsec** — the private key lives only in `n_signer`'s locked memory, and the agent pubkey is discovered at startup via `nostr_signer_get_public_key()`. ## Modes | Mode | Transport | Holds nsec in agent? | Persistable to genesis? | Installable as systemd? | |------|-----------|----------------------|-------------------------|-------------------------| | `local` | in-process | yes | yes | yes | | `nsigner_unix` | AF_UNIX abstract socket | no | yes | yes | | `nsigner_tcp` | TCP `host:port` | no | yes | yes | | `nsigner_serial` | USB CDC-ACM serial | no | yes | yes | | `nsigner_fds` | pre-connected fd pair | no | **no** (runtime-only) | **no** | | `nsigner_qrexec` | Qubes qrexec (cross-qube) | no | yes | yes | ### `local` Default, backward-compatible. The 32-byte private key is derived from `keys.nsec` and held in `cfg.keys.private_key`. Use this when you accept the nsec living in the agent process. ### `nsigner_unix` Delegates to `n_signer` over an abstract namespace Unix socket (`@`). If `socket_name` is empty, the agent auto-discovers running `n_signer` sockets via `nsigner_transport_list_unix()`. Best for same-host deployments where `n_signer` runs in an attended terminal. ### `nsigner_tcp` Delegates to `n_signer` over TCP. Requires `auth_privkey_hex` (64-char hex) for the kind-27235 auth envelope that `n_signer` enforces on TCP listeners. Use for cross-host or FIPS-mode deployments. ### `nsigner_serial` Delegates to `n_signer` over a USB CDC-ACM serial device (e.g. `/dev/ttyACM0`). The wizard auto-discovers `/dev/ttyACM*` devices via `nsigner_transport_list_serial()`. Use for air-gapped/hardware-token signing. ### `nsigner_fds` Delegates to `n_signer` over a pre-connected file-descriptor pair (`read_fd`, `write_fd`). Intended for qrexec-style fd-passing deployments. **Cannot be persisted to genesis** (fds are runtime-only) and **cannot be installed as a systemd service** (fds cannot be embedded in `ExecStart`). Supply via `--signer-fds ` at boot. ### `nsigner_qrexec` Delegates to `n_signer` running in **another Qubes OS qube** via qrexec. The agent spawns `qrexec-client-vm ` as a subprocess and pipes framed JSON-RPC through its stdin/stdout. No network; caller identity is hypervisor-vouched (`QREXEC_REMOTE_DOMAIN` → `qubes:` on the signer side). This is the recommended Qubes cross-qube signing path. Prerequisites (in the signer qube, e.g. `nostr_signer`): - `n_signer` running persistently: `nsigner --listen unix --socket-name nsigner --bridge-source-trusted` - The `qubes.NsignerRpc` qrexec service installed (see `n_signer/packaging/qubes/`) - dom0 qrexec policy allowing the agent qube to call the service The service name defaults to `qubes.NsignerRpc`; override with `--signer-service` or the `signer.service_name` config field. ## Configuration ### Genesis (`signer` block) ```jsonc "signer": { "mode": "nsigner_unix", "socket_name": "", // nsigner_unix: "" = auto-discover "role": "main", "timeout_ms": 15000, "auth_privkey_hex": "", // nsigner_tcp only "tcp_host": "127.0.0.1", // nsigner_tcp "tcp_port": 11111, // nsigner_tcp "serial_device": "/dev/ttyACM0", // nsigner_serial "target_qube": "nostr_signer", // nsigner_qrexec: target Qubes qube "service_name": "qubes.NsignerRpc" // nsigner_qrexec: qrexec service (default qubes.NsignerRpc) // nsigner_fds: fds_read_fd/fds_write_fd are CLI-only, NOT persisted here } ``` ### CLI flags | Flag | Purpose | |------|---------| | `--signer ` | Select signer mode (overrides config/env). | | `--signer-socket ` | Abstract socket name for `nsigner_unix`. | | `--signer-role ` | `n_signer` role selector (default `main`). | | `--signer-timeout ` | Per-call timeout (default `15000`). | | `--signer-tcp ` | Shorthand for `--signer nsigner_tcp` + host/port. | | `--signer-serial ` | Shorthand for `--signer nsigner_serial` + device. | | `--signer-fds ` | Shorthand for `--signer nsigner_fds` + fd pair. | | `--signer-qrexec ` | Shorthand for `--signer nsigner_qrexec` + target qube. | | `--signer-service ` | qrexec service name (default `qubes.NsignerRpc`). | Precedence: CLI flag > `DIDACTYL_SIGNER` env var > genesis `signer.mode` > default (`local`). In any `nsigner_*` mode, `--nsec` / `DIDACTYL_NSEC` are not required. ## Interactive setup wizard The wizard's **New agent → sign with a running n_signer** path and the **Existing agent** flow both present a transport picker: - `u` nix socket (auto-discovers `@nsigner*` abstract sockets) - `t` cp (prompts for `host:port` + optional auth privkey) - `s` erial (auto-discovers `/dev/ttyACM*`) - `f` d pair (prompts for `read_fd:write_fd` — advanced) - `e` xec qrexec (prompts for target qube + service name — Qubes cross-qube) After a transport is chosen, the wizard runs a connectivity check (`nostr_signer_get_public_key`) and populates the agent pubkey before continuing. The **Existing agent** flow additionally lets you recover an agent **without entering an nsec** — kind-30078 config recall/publish is routed through the signer verbs (`nostr_signer_nip44_decrypt` / `nostr_signer_nip44_encrypt`), so the nsec never enters the agent process during recovery. ## Systemd install `install_system_service_with_dedicated_user()` embeds the signer flags in `ExecStart` for `nsigner_unix`, `nsigner_tcp`, `nsigner_serial`, and `nsigner_qrexec`. `nsigner_fds` is rejected with a clear message (fds are runtime-only and cannot be embedded in `ExecStart`); choose **boot now** or wire fd-passing yourself via a wrapper unit. ## Security tradeoff `local` keeps the nsec in the agent process. If the agent is compromised, the nsec is exposed. Remote (`nsigner_*`) modes keep the nsec in `n_signer`'s mlock'd memory; a compromised agent can request signatures but cannot extract the raw key. The tradeoff is operational: `n_signer` must be running and reachable when the agent needs to sign.