139 lines
6.2 KiB
Markdown
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.
|