Files
didactyl/docs/SIGNER.md
T

6.2 KiB

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 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 (@<socket_name>). 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 <read:write> at boot.

nsigner_qrexec

Delegates to n_signer running in another Qubes OS qube via qrexec. The agent spawns qrexec-client-vm <target_qube> <service_name> as a subprocess and pipes framed JSON-RPC through its stdin/stdout. No network; caller identity is hypervisor-vouched (QREXEC_REMOTE_DOMAINqubes:<source-vm> 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)

"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 <mode> Select signer mode (overrides config/env).
--signer-socket <name> Abstract socket name for nsigner_unix.
--signer-role <role> n_signer role selector (default main).
--signer-timeout <ms> Per-call timeout (default 15000).
--signer-tcp <host:port> Shorthand for --signer nsigner_tcp + host/port.
--signer-serial <device> Shorthand for --signer nsigner_serial + device.
--signer-fds <read:write> Shorthand for --signer nsigner_fds + fd pair.
--signer-qrexec <target_qube> Shorthand for --signer nsigner_qrexec + target qube.
--signer-service <name> 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.