Files

9.1 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",                  // REQUIRED for all nsigner_* modes
  "role_path": "m/44'/1237'/0'/0/0", // REQUIRED for all nsigner_* modes: full BIP-44 path
  "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
  // derive_index: CLI-only (--signer-derive-index), NOT persisted here
}

Breaking change (n_signer v0.1.18+): the nostr_index selector is removed. n_signer now rejects it with 2006 nostr_index_deprecated. The only accepted selector for nostr_* verbs is {"role":"<name>", "role_path":"<full-path>"} sent together. Role-only is rejected with 2009 path_required; path-only with 2008 role_required. Update any existing genesis configs that use nostr_index to use role_path instead.

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). Required for all nsigner_* modes.
--signer-path <bip44-path> Full BIP-44 derivation path (e.g. m/44'/1237'/0'/0/0). Required for all nsigner_* modes — n_signer rejects role-only with 2009 path_required.
--signer-derive-index <n> Algorithm index for the derive (HMAC) verb. Runtime-only; not persisted to genesis.
--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. --signer-role and --signer-path are both 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 prompts for a role (default main) and a role_path (BIP-44 derivation path, with presets for NIP-06 standard m/44'/1237'/0'/0/0, all-hardened m/44'/1237'/0'/0'/0', or custom). Both are required — n_signer rejects role-only with 2009 path_required. Then the wizard runs a connectivity check (nostr_signer_get_public_key) and populates the agent pubkey before continuing. On failure, the n_signer error string is displayed.

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.

Agent tools (remote signer only)

Two agent tools leverage the remote signer for advanced operations:

nostr_post — PoW mining (NIP-13)

The nostr_post tool accepts optional difficulty, threads, and timeout_sec arguments. When difficulty > 0 and a remote signer is available, the event is mined through nostr_signer_mine_event before publishing. In local mode, PoW is not supported (the local backend does not implement mine_event); the tool returns a clear error.

signer_crypto — algorithm-based n_signer verbs

A single unified tool exposes all algorithm-based n_signer verbs through an operation enum:

Operation Algorithms Key args
get_info n/a
get_public_key all algorithm, index
sign secp256k1, ed25519, ml-dsa-65, slh-dsa-128s algorithm, index, scheme, message_hex
verify secp256k1, ed25519, ml-dsa-65, slh-dsa-128s algorithm, index, scheme, message_hex, signature_hex
encapsulate ml-kem-768 peer_pubkey_hex
decapsulate ml-kem-768 index, ciphertext_hex
derive_shared_secret x25519 index, peer_pubkey_hex
derive_hmac secp256k1 data (uses --signer-derive-index)
otp_encrypt otp plaintext_b64, encoding
otp_decrypt otp ciphertext, encoding

All operations require a remote n_signer; local mode returns NOSTR_ERROR_NOT_SUPPORTED. See the n_signer README §4 for the full algorithm reference.

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.