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_DOMAIN → qubes:<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_signerrunning persistently:nsigner --listen unix --socket-name nsigner --bridge-source-trusted- The
qubes.NsignerRpcqrexec service installed (seen_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:
unix socket (auto-discovers@nsigner*abstract sockets)tcp (prompts forhost:port+ optional auth privkey)serial (auto-discovers/dev/ttyACM*)fd pair (prompts forread_fd:write_fd— advanced)exec 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.