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_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", // 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_indexselector is removed. n_signer now rejects it with2006 nostr_index_deprecated. The only accepted selector fornostr_*verbs is{"role":"<name>", "role_path":"<full-path>"}sent together. Role-only is rejected with2009 path_required; path-only with2008 role_required. Update any existing genesis configs that usenostr_indexto userole_pathinstead.
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:
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 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.