Files
n_signer/client/n_signer_client.c
T
Laan Tungir 9afbb8fcbd Refactor n_signer_client to use nostr_core_lib high-level wrappers
The CLI was hand-building cJSON params and calling the low-level
nsigner_client_call for all 16 verbs. Now it uses the high-level
nostr_signer_t typed wrappers from nostr_core_lib for 14 of 16 verbs:
get_info, get_public_key (alg + nostr), sign_event, mine_event,
nip04/44 encrypt+decrypt, sign, verify, derive, encapsulate,
decapsulate, derive_shared_secret, otp encrypt/decrypt.

The 'call' verb (raw passthrough) and 'derive --algorithm' still use
the low-level nsigner_client_call on the shared connection (created via
nostr_signer_nsigner_from_client). The 'list' verb uses
nsigner_transport_list_unix directly.

The client shrank from 945 to ~840 lines, with the per-verb cJSON
building logic now in the library. Error messages use
nostr_signer_last_error() to surface the raw n_signer RPC error text
(path_not_allowed, unknown_role, etc.).

Also adds two plan docs:
- plans/client_breaking_change_audit.md: audit of n_signer breaking
  changes vs all ~/lt/ client repos
- plans/nostr_core_lib_full_verb_coverage.md: analysis of the library
  verb coverage gap that motivated this refactor

Tests: 45/45 pass, 0 fail, 2 skip (test_n_signer_client.sh).
2026-08-06 10:17:47 -04:00

853 lines
36 KiB
C

/*
* n_signer_client.c — standalone Linux CLI for n_signer JSON-RPC API.
*
* Connects to a running n_signer process over its abstract UNIX socket
* (or TCP/serial/qrexec) and exposes the full verb surface over stdin/stdout
* so that signed events can be piped directly into `nak publish`.
*
* This client uses the high-level nostr_signer_t API from nostr_core_lib
* for all typed verbs. The per-verb cJSON-building logic lives in the
* library, not here. The CLI is mostly argv parsing + result printing.
*
* Build: make clients
* Usage: n_signer_client [global options] <verb> [verb args...]
*
* See client/n_signer_client_README.md for full documentation.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include "nostr_common.h"
#include "nsigner_transport.h"
#include "nsigner_client.h"
#include "nostr_signer.h"
#include "../cjson/cJSON.h"
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static void print_usage(FILE *fp, const char *prog) {
fprintf(fp,
"Usage: %s [global options] <verb> [verb args...]\n"
"\n"
"Global options:\n"
" -n, --socket-name <name> Abstract socket name (default: auto-discover)\n"
" --timeout <ms> Transport timeout (default 5000)\n"
" --tcp <host:port> TCP transport (requires --auth-privkey)\n"
" --serial <device> USB CDC-ACM serial transport\n"
" --qrexec <qube:svc> Qubes qrexec transport\n"
" --auth-privkey <hex> Auth envelope privkey (32 bytes hex)\n"
" --auth-label <text> Auth envelope label\n"
"\n"
"Selector options (for nostr_* verbs):\n"
" --role <name> Named path-role\n"
" --path <path> Full BIP-44 derivation path\n"
"\n"
"Algorithm options (for algorithm-based verbs):\n"
" -a, --algorithm <alg> secp256k1/ed25519/x25519/ml-dsa-65/\n"
" slh-dsa-128s/ml-kem-768/otp\n"
" --scheme <schnorr|ecdsa> secp256k1 sign/verify scheme (default schnorr)\n"
" --encoding <ascii|binary> OTP encoding (default ascii)\n"
" --format <plain|structured> get-public-key output (default plain)\n"
" --index <N> Algorithm derivation index\n"
"\n"
"Mine-event options:\n"
" --difficulty <N> Target leading zero bits\n"
" --threads <N> Mining threads (default 1)\n"
" --timeout-sec <N> Mining timeout in seconds\n"
"\n"
"Verbs:\n"
" list List running n_signer sockets\n"
" get-info\n"
" get-public-key\n"
" sign-event\n"
" mine-event\n"
" nip04-encrypt <peer-pubkey>\n"
" nip04-decrypt <peer-pubkey>\n"
" nip44-encrypt <peer-pubkey>\n"
" nip44-decrypt <peer-pubkey>\n"
" sign <msg-hex>\n"
" verify <msg-hex> <sig-hex>\n"
" derive <data>\n"
" encapsulate <peer-pubkey-hex>\n"
" decapsulate <ciphertext-hex>\n"
" derive-shared-secret <peer-pubkey-hex>\n"
" encrypt <plaintext>\n"
" decrypt <ciphertext>\n"
" call <method>\n"
"\n"
"Examples:\n"
" # List running n_signer sockets\n"
" %s list\n"
"\n"
" # Get a Nostr public key by role and path\n"
" %s --role main --path \"m/44'/1237'/0'/0/0\" get-public-key\n"
"\n"
" # Get a key by named path-role\n"
" %s --role role1 --path \"m/44'/1237'/1'/1/0\" get-public-key\n"
"\n"
" # Sign a Nostr event from stdin and pipe to nak for publishing\n"
" echo '{\"kind\":1,\"content\":\"hello world\",\"tags\":[],\"created_at\":1700000000}' \\\n"
" | %s --role main --path \"m/44'/1237'/0'/0/0\" sign-event | nak publish\n"
"\n"
" # Sign an event from argv\n"
" %s --role main --path \"m/44'/1237'/0'/0/0\" sign-event '{\"kind\":1,\"content\":\"hi\",\"tags\":[],\"created_at\":1700000000}'\n"
"\n"
" # Mine an event with proof-of-work (difficulty 20)\n"
" %s --role main --path \"m/44'/1237'/0'/0/0\" --difficulty 20 mine-event '{\"kind\":1,\"content\":\"mined\",\"tags\":[],\"created_at\":1700000000}'\n"
"\n"
" # NIP-44 encrypt then decrypt a round-trip\n"
" %s --role main --path \"m/44'/1237'/0'/0/0\" nip44-encrypt <peer-pubkey> 'secret message'\n"
" %s --role main --path \"m/44'/1237'/0'/0/0\" nip44-decrypt <peer-pubkey> '<ciphertext>'\n"
"\n"
" # Ed25519 sign (SSH-style)\n"
" %s --algorithm ed25519 --index 0 sign 68656c6c6f\n"
"\n"
" # Get signer metadata\n"
" %s get-info\n",
prog, prog, prog, prog, prog, prog, prog, prog, prog, prog, prog);
}
/* Read one line from stdin (newline stripped). Returns malloc'd string or NULL on EOF/error. */
static char *read_stdin_line(void) {
size_t cap = 4096;
size_t len = 0;
char *buf = malloc(cap);
if (!buf) return NULL;
int c;
while ((c = fgetc(stdin)) != EOF && c != '\n') {
if (len + 1 >= cap) {
cap *= 2;
char *tmp = realloc(buf, cap);
if (!tmp) { free(buf); return NULL; }
buf = tmp;
}
buf[len++] = (char)c;
}
if (len == 0 && c == EOF) { free(buf); return NULL; }
buf[len] = '\0';
return buf;
}
/* Convert a hex string to raw bytes. Returns number of bytes written, or -1 on error. */
static int hex_to_bytes(const char *hex, unsigned char *out, size_t out_sz) {
size_t len = strlen(hex);
if (len % 2 != 0 || len / 2 > out_sz) return -1;
for (size_t i = 0; i < len / 2; i++) {
unsigned int byte;
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) return -1;
out[i] = (unsigned char)byte;
}
return (int)(len / 2);
}
/* Parse "host:port" string. Returns 0 on success. */
static int parse_host_port(const char *s, char **out_host, int *out_port) {
const char *colon = strrchr(s, ':');
if (!colon || colon == s) return -1;
size_t host_len = (size_t)(colon - s);
*out_host = malloc(host_len + 1);
if (!*out_host) return -1;
memcpy(*out_host, s, host_len);
(*out_host)[host_len] = '\0';
char *end = NULL;
long p = strtol(colon + 1, &end, 10);
if (end == colon + 1 || *end != '\0' || p < 1 || p > 65535) {
free(*out_host);
*out_host = NULL;
return -1;
}
*out_port = (int)p;
return 0;
}
/* Parse "qube:service" string. Returns 0 on success. */
static int parse_qube_service(const char *s, char **out_qube, char **out_service) {
const char *colon = strchr(s, ':');
if (!colon || colon == s) return -1;
size_t qube_len = (size_t)(colon - s);
*out_qube = malloc(qube_len + 1);
if (!*out_qube) return -1;
memcpy(*out_qube, s, qube_len);
(*out_qube)[qube_len] = '\0';
*out_service = strdup(colon + 1);
if (!*out_service) { free(*out_qube); *out_qube = NULL; return -1; }
return 0;
}
/* ------------------------------------------------------------------ */
/* Result printing helper */
/* ------------------------------------------------------------------ */
/* Print a raw string result (from the *_result_json_out wrappers). */
static void print_result_str(const char *s) {
if (s) {
printf("%s\n", s);
} else {
printf("null\n");
}
}
/* ------------------------------------------------------------------ */
/* Transport setup helper */
/* ------------------------------------------------------------------ */
/* Opens a transport based on the CLI args. Returns 0 on success.
* On success, *out_transport is set (caller must not free if handed to signer). */
static int open_transport(const char *socket_name, int timeout_ms,
const char *tcp_arg, const char *serial_arg,
const char *qrexec_arg,
const char *auth_privkey_hex,
nsigner_transport_t **out_transport) {
int transport_count = (tcp_arg ? 1 : 0) + (serial_arg ? 1 : 0) + (qrexec_arg ? 1 : 0) + (socket_name ? 1 : 0);
if (transport_count > 1) {
fprintf(stderr, "error: --tcp, --serial, --qrexec, and --socket-name are mutually exclusive\n");
return -1;
}
*out_transport = NULL;
if (tcp_arg) {
if (!auth_privkey_hex) {
fprintf(stderr, "error: --tcp requires --auth-privkey\n");
return -1;
}
char *host = NULL;
int port = 0;
if (parse_host_port(tcp_arg, &host, &port) != 0) {
fprintf(stderr, "error: invalid --tcp format (expected host:port)\n");
return -1;
}
*out_transport = nsigner_transport_open_tcp(host, port, timeout_ms);
free(host);
if (!*out_transport) {
fprintf(stderr, "error: cannot open TCP transport to %s\n", tcp_arg);
return -1;
}
} else if (serial_arg) {
*out_transport = nsigner_transport_open_serial(serial_arg, timeout_ms);
if (!*out_transport) {
fprintf(stderr, "error: cannot open serial transport on %s\n", serial_arg);
return -1;
}
} else if (qrexec_arg) {
char *qube = NULL, *service = NULL;
if (parse_qube_service(qrexec_arg, &qube, &service) != 0) {
fprintf(stderr, "error: invalid --qrexec format (expected qube:service)\n");
return -1;
}
*out_transport = nsigner_transport_open_qrexec(qube, service, timeout_ms);
free(qube);
free(service);
if (!*out_transport) {
fprintf(stderr, "error: cannot open qrexec transport to %s\n", qrexec_arg);
return -1;
}
} else if (socket_name) {
*out_transport = nsigner_transport_open_unix(socket_name, timeout_ms);
if (!*out_transport) {
fprintf(stderr, "error: cannot open unix transport %s\n", socket_name);
return -1;
}
} else {
/* Auto-discover: enumerate abstract UNIX sockets */
char names[64][64];
int count = nsigner_transport_list_unix(names, 64);
if (count == 0) {
fprintf(stderr, "error: no n_signer sockets found. Is n_signer running?\n");
return -1;
}
if (count > 1) {
fprintf(stderr, "error: multiple n_signer sockets found. Use --socket-name to select one:\n");
for (int j = 0; j < count; j++) {
fprintf(stderr, " %s\n", names[j]);
}
return -1;
}
*out_transport = nsigner_transport_open_unix(names[0], timeout_ms);
if (!*out_transport) {
fprintf(stderr, "error: cannot open unix transport %s\n", names[0]);
return -1;
}
}
return 0;
}
/* ------------------------------------------------------------------ */
/* Main */
/* ------------------------------------------------------------------ */
int main(int argc, char **argv) {
/* ---- globals ---- */
const char *socket_name = NULL;
int timeout_ms = 5000;
const char *tcp_arg = NULL;
const char *serial_arg = NULL;
const char *qrexec_arg = NULL;
const char *auth_privkey_hex = NULL;
const char *auth_label = NULL;
/* ---- selectors (nostr verbs) ---- */
const char *role = NULL;
const char *path = NULL;
int has_index = 0;
int index_val = 0;
/* ---- algorithm options ---- */
const char *algorithm = NULL;
int alg_index = 0;
int has_alg_index = 0;
const char *scheme = NULL;
const char *encoding = NULL;
const char *format = NULL;
/* ---- mine-event options ---- */
int has_difficulty = 0;
int difficulty_val = 0;
int has_threads = 0;
int threads_val = 1;
int has_timeout_sec = 0;
int timeout_sec_val = 0;
const char *prog = argv[0];
/* ---- parse global options ---- */
int i = 1;
while (i < argc && argv[i][0] == '-') {
const char *arg = argv[i];
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
print_usage(stderr, prog);
return 2;
}
if (strcmp(arg, "--socket-name") == 0 || strcmp(arg, "-n") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --socket-name requires an argument\n"); return 2; }
socket_name = argv[++i];
} else if (strcmp(arg, "--timeout") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --timeout requires an argument\n"); return 2; }
timeout_ms = atoi(argv[++i]);
if (timeout_ms <= 0) { fprintf(stderr, "error: --timeout must be positive\n"); return 2; }
} else if (strcmp(arg, "--tcp") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --tcp requires <host:port>\n"); return 2; }
tcp_arg = argv[++i];
} else if (strcmp(arg, "--serial") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --serial requires a device path\n"); return 2; }
serial_arg = argv[++i];
} else if (strcmp(arg, "--qrexec") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --qrexec requires <qube:service>\n"); return 2; }
qrexec_arg = argv[++i];
} else if (strcmp(arg, "--auth-privkey") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --auth-privkey requires a 32-byte hex key\n"); return 2; }
auth_privkey_hex = argv[++i];
} else if (strcmp(arg, "--auth-label") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --auth-label requires a label\n"); return 2; }
auth_label = argv[++i];
} else if (strcmp(arg, "--role") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --role requires a name\n"); return 2; }
role = argv[++i];
} else if (strcmp(arg, "--path") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --path requires a BIP-44 derivation path\n"); return 2; }
path = argv[++i];
} else if (strcmp(arg, "--index") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --index requires a number\n"); return 2; }
has_index = 1;
index_val = atoi(argv[++i]);
} else if (strcmp(arg, "--algorithm") == 0 || strcmp(arg, "-a") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --algorithm requires a name\n"); return 2; }
algorithm = argv[++i];
} else if (strcmp(arg, "--scheme") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --scheme requires schnorr or edsa\n"); return 2; }
scheme = argv[++i];
} else if (strcmp(arg, "--encoding") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --encoding requires ascii or binary\n"); return 2; }
encoding = argv[++i];
} else if (strcmp(arg, "--format") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --format requires plain or structured\n"); return 2; }
format = argv[++i];
} else if (strcmp(arg, "--difficulty") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --difficulty requires a number\n"); return 2; }
has_difficulty = 1;
difficulty_val = atoi(argv[++i]);
} else if (strcmp(arg, "--threads") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --threads requires a number\n"); return 2; }
has_threads = 1;
threads_val = atoi(argv[++i]);
} else if (strcmp(arg, "--timeout-sec") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --timeout-sec requires a number\n"); return 2; }
has_timeout_sec = 1;
timeout_sec_val = atoi(argv[++i]);
} else {
fprintf(stderr, "error: unknown option: %s\n", arg);
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
return 2;
}
i++;
}
/* ---- verb ---- */
if (i >= argc) {
fprintf(stderr, "error: no verb specified\n");
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
return 2;
}
const char *verb = argv[i++];
/* ---- verb args ---- */
const char *arg1 = (i < argc) ? argv[i++] : NULL;
const char *arg2 = (i < argc) ? argv[i++] : NULL;
/* ---- validate --index usage (algorithm-only now) ---- */
if (has_index && !algorithm) {
fprintf(stderr, "error: --index is only valid with --algorithm (for algorithm verbs)\n");
return 2;
}
/* ---- determine if this is an algorithm verb ---- */
int is_algorithm_verb = (algorithm != NULL);
/* ---- validate --role and --path for nostr verbs ---- */
int is_nostr_verb = (strcmp(verb, "get-public-key") == 0 ||
strcmp(verb, "sign-event") == 0 ||
strcmp(verb, "mine-event") == 0 ||
strcmp(verb, "nip04-encrypt") == 0 ||
strcmp(verb, "nip04-decrypt") == 0 ||
strcmp(verb, "nip44-encrypt") == 0 ||
strcmp(verb, "nip44-decrypt") == 0);
if (is_nostr_verb && !is_algorithm_verb) {
if (!role) {
fprintf(stderr, "error: --role is required for nostr verbs\n");
return 2;
}
if (!path) {
fprintf(stderr, "error: --path is required for nostr verbs\n");
return 2;
}
}
/* ---- nostr_init ---- */
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "error: failed to initialize crypto subsystem\n");
return 2;
}
/* ---- list verb (no connection needed) ---- */
if (strcmp(verb, "list") == 0) {
char names[64][64];
int count = nsigner_transport_list_unix(names, 64);
if (count == 0) {
printf("no n_signer sockets found\n");
} else {
for (int j = 0; j < count; j++) {
printf("%s\n", names[j]);
}
}
nostr_cleanup();
return 0;
}
/* ---- open transport ---- */
nsigner_transport_t *transport = NULL;
if (open_transport(socket_name, timeout_ms, tcp_arg, serial_arg, qrexec_arg,
auth_privkey_hex, &transport) != 0) {
nostr_cleanup();
return 2;
}
/* ---- create low-level client (owns transport) ---- */
nsigner_client_t *client = nsigner_client_new(transport);
if (!client) {
fprintf(stderr, "error: cannot create nsigner client\n");
transport->close(transport);
nostr_cleanup();
return 2;
}
transport = NULL; /* owned by client */
/* ---- create high-level signer from client (shares the connection) ---- */
nostr_signer_t *signer = nostr_signer_nsigner_from_client(client, role);
if (!signer) {
fprintf(stderr, "error: cannot create nsigner signer\n");
nsigner_client_free(client);
nostr_cleanup();
return 2;
}
/* signer now owns client; don't free it separately */
/* ---- set role_path selector for nostr verbs ---- */
if (is_nostr_verb && !is_algorithm_verb && path) {
if (nostr_signer_nsigner_set_role_path(signer, path) != NOSTR_SUCCESS) {
fprintf(stderr, "error: failed to set role_path\n");
nostr_signer_free(signer);
nostr_cleanup();
return 2;
}
}
/* ---- auth envelope (TCP) ---- */
if (auth_privkey_hex) {
unsigned char privkey[32];
if (hex_to_bytes(auth_privkey_hex, privkey, 32) != 32) {
fprintf(stderr, "error: --auth-privkey must be 32 bytes (64 hex chars)\n");
nostr_signer_free(signer);
nostr_cleanup();
return 2;
}
if (nostr_signer_nsigner_set_auth(signer, privkey, auth_label ? auth_label : "") != NOSTR_SUCCESS) {
fprintf(stderr, "error: failed to set auth envelope\n");
nostr_signer_free(signer);
nostr_cleanup();
return 2;
}
}
/* ---- resolve algorithm index ---- */
int eff_index = has_alg_index ? alg_index : (has_index ? index_val : 0);
int rc = 2;
char *result_str = NULL;
cJSON *result_obj = NULL;
/* ---- dispatch verbs via high-level library wrappers ---- */
if (strcmp(verb, "get-info") == 0) {
rc = nostr_signer_get_info(signer, &result_obj);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
goto cleanup;
}
char *json = cJSON_PrintUnformatted(result_obj);
if (json) { printf("%s\n", json); free(json); }
rc = 0;
} else if (strcmp(verb, "get-public-key") == 0) {
if (is_algorithm_verb) {
rc = nostr_signer_get_public_key_alg(signer, algorithm, eff_index, &result_str);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
goto cleanup;
}
print_result_str(result_str);
rc = 0;
} else if (format && strcmp(format, "structured") == 0) {
/* Structured format: use low-level client to pass the format option. */
cJSON *params = cJSON_CreateArray();
cJSON *opts = cJSON_CreateObject();
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
cJSON_AddStringToObject(opts, "format", "structured");
cJSON_AddItemToArray(params, opts);
cJSON *presult = NULL;
rc = nsigner_client_call(client, "nostr_get_public_key", params, &presult);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nsigner_client_last_error(client)); goto cleanup; }
if (cJSON_IsString(presult)) {
print_result_str(presult->valuestring);
} else if (presult) {
char *json = cJSON_PrintUnformatted(presult);
if (json) { printf("%s\n", json); free(json); }
}
cJSON_Delete(presult);
rc = 0;
} else {
char pubkey_hex[65];
rc = nostr_signer_get_public_key(signer, pubkey_hex);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
goto cleanup;
}
printf("%s\n", pubkey_hex);
rc = 0;
}
} else if (strcmp(verb, "sign-event") == 0) {
const char *event_json = arg1;
char *event_buf = NULL;
if (!event_json) {
event_buf = read_stdin_line();
if (!event_buf) {
fprintf(stderr, "error: no event JSON provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
event_json = event_buf;
}
cJSON *event = cJSON_Parse(event_json);
free(event_buf);
if (!event) {
fprintf(stderr, "error: failed to parse event JSON\n");
goto cleanup;
}
rc = nostr_signer_sign_event(signer, event, &result_obj);
cJSON_Delete(event);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
goto cleanup;
}
char *json = cJSON_PrintUnformatted(result_obj);
if (json) { printf("%s\n", json); free(json); }
rc = 0;
} else if (strcmp(verb, "mine-event") == 0) {
const char *event_json = arg1;
char *event_buf = NULL;
if (!event_json) {
event_buf = read_stdin_line();
if (!event_buf) {
fprintf(stderr, "error: no event JSON provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
event_json = event_buf;
}
cJSON *event = cJSON_Parse(event_json);
free(event_buf);
if (!event) {
fprintf(stderr, "error: failed to parse event JSON\n");
goto cleanup;
}
rc = nostr_signer_mine_event(signer, event,
has_difficulty ? difficulty_val : 0,
has_timeout_sec ? timeout_sec_val : 0,
has_threads ? threads_val : 1,
&result_obj);
cJSON_Delete(event);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer));
goto cleanup;
}
char *json = cJSON_PrintUnformatted(result_obj);
if (json) { printf("%s\n", json); free(json); }
rc = 0;
} else if (strcmp(verb, "nip04-encrypt") == 0) {
if (!arg1) { fprintf(stderr, "error: nip04-encrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *plaintext = arg2;
char *pt_buf = NULL;
if (!plaintext) {
pt_buf = read_stdin_line();
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
plaintext = pt_buf;
}
rc = nostr_signer_nip04_encrypt(signer, arg1, plaintext, &result_str);
free(pt_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "nip04-decrypt") == 0) {
if (!arg1) { fprintf(stderr, "error: nip04-decrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *ciphertext = arg2;
char *ct_buf = NULL;
if (!ciphertext) {
ct_buf = read_stdin_line();
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
ciphertext = ct_buf;
}
rc = nostr_signer_nip04_decrypt(signer, arg1, ciphertext, &result_str);
free(ct_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "nip44-encrypt") == 0) {
if (!arg1) { fprintf(stderr, "error: nip44-encrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *plaintext = arg2;
char *pt_buf = NULL;
if (!plaintext) {
pt_buf = read_stdin_line();
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
plaintext = pt_buf;
}
rc = nostr_signer_nip44_encrypt(signer, arg1, plaintext, &result_str);
free(pt_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "nip44-decrypt") == 0) {
if (!arg1) { fprintf(stderr, "error: nip44-decrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *ciphertext = arg2;
char *ct_buf = NULL;
if (!ciphertext) {
ct_buf = read_stdin_line();
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
ciphertext = ct_buf;
}
rc = nostr_signer_nip44_decrypt(signer, arg1, ciphertext, &result_str);
free(ct_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "sign") == 0) {
if (!arg1) { fprintf(stderr, "error: sign requires <msg-hex>\n"); goto cleanup; }
size_t msg_len = strlen(arg1) / 2;
unsigned char *msg = malloc(msg_len ? msg_len : 1);
if (!msg) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
int n = hex_to_bytes(arg1, msg, msg_len);
if (n < 0) { free(msg); fprintf(stderr, "error: invalid hex message\n"); goto cleanup; }
rc = nostr_signer_sign(signer, algorithm ? algorithm : "secp256k1",
eff_index, scheme, msg, (size_t)n, &result_str);
free(msg);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "verify") == 0) {
if (!arg1 || !arg2) { fprintf(stderr, "error: verify requires <msg-hex> <sig-hex>\n"); goto cleanup; }
size_t msg_len = strlen(arg1) / 2;
size_t sig_len = strlen(arg2) / 2;
unsigned char *msg = malloc(msg_len ? msg_len : 1);
unsigned char *sig = malloc(sig_len ? sig_len : 1);
if (!msg || !sig) { free(msg); free(sig); fprintf(stderr, "error: out of memory\n"); goto cleanup; }
int mn = hex_to_bytes(arg1, msg, msg_len);
int sn = hex_to_bytes(arg2, sig, sig_len);
if (mn < 0 || sn < 0) { free(msg); free(sig); fprintf(stderr, "error: invalid hex\n"); goto cleanup; }
int valid = 0;
rc = nostr_signer_verify(signer, algorithm ? algorithm : "secp256k1",
eff_index, scheme, msg, (size_t)mn, sig, (size_t)sn, &valid);
free(msg);
free(sig);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
printf("%s\n", valid ? "valid" : "invalid");
rc = valid ? 0 : 1;
} else if (strcmp(verb, "derive") == 0) {
const char *data = arg1;
char *data_buf = NULL;
if (!data) {
data_buf = read_stdin_line();
if (!data_buf) { fprintf(stderr, "error: no data provided\n"); goto cleanup; }
data = data_buf;
}
if (is_algorithm_verb) {
/* Algorithm-based derive: use low-level client to pass algorithm+index. */
cJSON *params = cJSON_CreateArray();
cJSON_AddItemToArray(params, cJSON_CreateString(data));
cJSON *opts = cJSON_CreateObject();
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "secp256k1");
cJSON_AddNumberToObject(opts, "index", eff_index);
cJSON_AddItemToArray(params, opts);
cJSON *dresult = NULL;
rc = nsigner_client_call(client, "derive", params, &dresult);
free(data_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nsigner_client_last_error(client)); goto cleanup; }
if (cJSON_IsString(dresult)) {
/* The derive result is a JSON object string like
* {"algorithm":"secp256k1","key_id":"...","digest":"<64hex>"}.
* Print the raw result string. */
print_result_str(dresult->valuestring);
}
cJSON_Delete(dresult);
rc = 0;
} else {
/* Nostr derive (HMAC): use the high-level wrapper. */
char digest_hex[65];
rc = nostr_signer_derive_hmac(signer, data, digest_hex);
free(data_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
printf("%s\n", digest_hex);
rc = 0;
}
} else if (strcmp(verb, "encapsulate") == 0) {
if (!arg1) { fprintf(stderr, "error: encapsulate requires <peer-pubkey-hex>\n"); goto cleanup; }
rc = nostr_signer_encapsulate(signer, arg1, &result_str);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "decapsulate") == 0) {
if (!arg1) { fprintf(stderr, "error: decapsulate requires <ciphertext-hex>\n"); goto cleanup; }
rc = nostr_signer_decapsulate(signer, eff_index, arg1, &result_str);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "derive-shared-secret") == 0) {
if (!arg1) { fprintf(stderr, "error: derive-shared-secret requires <peer-pubkey-hex>\n"); goto cleanup; }
rc = nostr_signer_derive_shared_secret(signer, eff_index, arg1, &result_str);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "encrypt") == 0) {
const char *plaintext = arg1;
char *pt_buf = NULL;
if (!plaintext) {
pt_buf = read_stdin_line();
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
plaintext = pt_buf;
}
rc = nostr_signer_otp_encrypt(signer, plaintext, encoding, &result_str);
free(pt_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "decrypt") == 0) {
const char *ciphertext = arg1;
char *ct_buf = NULL;
if (!ciphertext) {
ct_buf = read_stdin_line();
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
ciphertext = ct_buf;
}
rc = nostr_signer_otp_decrypt(signer, ciphertext, encoding, &result_str);
free(ct_buf);
if (rc != NOSTR_SUCCESS) { fprintf(stderr, "error: %s\n", nostr_signer_last_error(signer)); goto cleanup; }
print_result_str(result_str);
rc = 0;
} else if (strcmp(verb, "call") == 0) {
/* Raw passthrough using the low-level client (shared with signer). */
if (!arg1) { fprintf(stderr, "error: call requires <method>\n"); goto cleanup; }
const char *method = arg1;
cJSON *params = NULL;
if (arg2) {
size_t total = 0;
for (int j = i - 1; j < argc; j++) {
total += strlen(argv[j]) + 1;
}
char *json_str = malloc(total + 1);
if (!json_str) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
json_str[0] = '\0';
for (int j = i - 1; j < argc; j++) {
strcat(json_str, argv[j]);
if (j + 1 < argc) strcat(json_str, " ");
}
params = cJSON_Parse(json_str);
free(json_str);
if (!params) {
fprintf(stderr, "error: failed to parse params JSON from argv\n");
goto cleanup;
}
} else {
char *line = read_stdin_line();
if (!line) {
fprintf(stderr, "error: no params JSON on stdin\n");
goto cleanup;
}
params = cJSON_Parse(line);
free(line);
if (!params) {
fprintf(stderr, "error: failed to parse params JSON from stdin\n");
goto cleanup;
}
}
cJSON *call_result = NULL;
if (nsigner_client_call(client, method, params, &call_result) != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nsigner_client_last_error(client));
goto cleanup;
}
if (call_result) {
char *json = cJSON_PrintUnformatted(call_result);
if (json) { printf("%s\n", json); free(json); }
cJSON_Delete(call_result);
}
rc = 0;
} else {
fprintf(stderr, "error: unknown verb: %s\n", verb);
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
goto cleanup;
}
cleanup:
if (result_str) free(result_str);
if (result_obj) cJSON_Delete(result_obj);
if (signer) nostr_signer_free(signer);
nostr_cleanup();
return rc;
}