Files
didactyl/docs/SIGNER.md
T

139 lines
6.2 KiB
Markdown

# 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",
"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.