# 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
(`@<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_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",                  // 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.
