189 lines
9.1 KiB
Markdown
189 lines
9.1 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", // 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.
|