Files
n_signer/client/n_signer_client.c
T

945 lines
39 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`.
*
* 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 "../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 <base64|hex> OTP encoding (default base64)\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 cJSON result value to stdout as a single newline-terminated line.
* Returns 0 for "valid" / 1 for "invalid" on verify verbs, -1 otherwise.
*/
static int print_result(cJSON *result, int is_verify) {
if (!result) {
printf("null\n");
return -1;
}
if (is_verify) {
/* verify result: cJSON string containing JSON object like {"valid":true,...}
* or a plain string "valid"/"invalid" */
if (cJSON_IsString(result)) {
const char *s = result->valuestring;
/* Try parsing as JSON object */
cJSON *parsed = cJSON_Parse(s);
if (parsed) {
cJSON *v = cJSON_GetObjectItemCaseSensitive(parsed, "valid");
if (v && cJSON_IsBool(v)) {
printf("%s\n", cJSON_IsTrue(v) ? "valid" : "invalid");
cJSON_Delete(parsed);
return cJSON_IsTrue(v) ? 0 : 1;
}
cJSON_Delete(parsed);
}
/* Fallback: check string value */
if (strcmp(s, "valid") == 0 || strcmp(s, "true") == 0) {
printf("valid\n");
return 0;
}
printf("invalid\n");
return 1;
}
if (cJSON_IsBool(result)) {
printf("%s\n", cJSON_IsTrue(result) ? "valid" : "invalid");
return cJSON_IsTrue(result) ? 0 : 1;
}
printf("invalid\n");
return 1;
}
if (cJSON_IsString(result)) {
printf("%s\n", result->valuestring);
} else if (cJSON_IsObject(result) || cJSON_IsArray(result)) {
char *json = cJSON_PrintUnformatted(result);
if (json) {
printf("%s\n", json);
free(json);
}
} else if (cJSON_IsNumber(result)) {
/* Use valuedouble for all numbers; cJSON stores ints as doubles internally */
double d = result->valuedouble;
if (d == (double)(int)d) {
printf("%d\n", (int)d);
} else {
printf("%g\n", d);
}
} else if (cJSON_IsTrue(result)) {
printf("true\n");
} else if (cJSON_IsFalse(result)) {
printf("false\n");
} else if (cJSON_IsNull(result)) {
printf("null\n");
} else {
printf("\n");
}
return -1;
}
/* ------------------------------------------------------------------ */
/* 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 ecdsa\n"); return 2; }
scheme = argv[++i];
} else if (strcmp(arg, "--encoding") == 0) {
if (i + 1 >= argc) { fprintf(stderr, "error: --encoding requires base64 or hex\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);
/* ---- nostr_get_public_key with --format structured uses algorithm path too,
* but it's still a nostr verb. The --format flag only applies to nostr_get_public_key.
* If --algorithm is set, get-public-key becomes an algorithm verb. */
int is_nostr_get_pubkey_structured = 0;
if (!is_algorithm_verb && format && strcmp(format, "structured") == 0) {
is_nostr_get_pubkey_structured = 1;
}
/* ---- 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;
}
/* ---- transport setup ---- */
nsigner_transport_t *transport = NULL;
nsigner_client_t *client = NULL;
cJSON *params = NULL;
cJSON *result = NULL;
int rc = 2;
int is_verify = 0;
/* Determine transport type */
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");
goto cleanup;
}
if (tcp_arg) {
/* TCP transport */
if (!auth_privkey_hex) {
fprintf(stderr, "error: --tcp requires --auth-privkey\n");
goto cleanup;
}
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");
goto cleanup;
}
transport = nsigner_transport_open_tcp(host, port, timeout_ms);
free(host);
if (!transport) {
fprintf(stderr, "error: cannot open TCP transport to %s\n", tcp_arg);
goto cleanup;
}
} else if (serial_arg) {
transport = nsigner_transport_open_serial(serial_arg, timeout_ms);
if (!transport) {
fprintf(stderr, "error: cannot open serial transport on %s\n", serial_arg);
goto cleanup;
}
} 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");
goto cleanup;
}
transport = nsigner_transport_open_qrexec(qube, service, timeout_ms);
free(qube);
free(service);
if (!transport) {
fprintf(stderr, "error: cannot open qrexec transport to %s\n", qrexec_arg);
goto cleanup;
}
} else if (socket_name) {
transport = nsigner_transport_open_unix(socket_name, timeout_ms);
if (!transport) {
fprintf(stderr, "error: cannot open unix transport %s\n", socket_name);
goto cleanup;
}
} 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");
goto cleanup;
}
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]);
}
goto cleanup;
}
socket_name = names[0];
transport = nsigner_transport_open_unix(socket_name, timeout_ms);
if (!transport) {
fprintf(stderr, "error: cannot open unix transport %s\n", socket_name);
goto cleanup;
}
}
/* ---- create client ---- */
client = nsigner_client_new(transport);
if (!client) {
fprintf(stderr, "error: cannot create nsigner client\n");
transport->close(transport);
goto cleanup;
}
transport = NULL; /* owned by client */
/* ---- 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");
goto cleanup;
}
if (nsigner_client_set_auth(client, privkey, auth_label ? auth_label : "") != NOSTR_SUCCESS) {
fprintf(stderr, "error: failed to set auth envelope\n");
goto cleanup;
}
}
/* ---- build params and call ---- */
const char *method = NULL;
int is_call_verb = 0;
if (strcmp(verb, "get-info") == 0) {
method = "get_info";
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
} else if (strcmp(verb, "get-public-key") == 0) {
if (is_algorithm_verb) {
method = "get_public_key";
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm);
if (has_alg_index) {
cJSON_AddNumberToObject(opts, "index", alg_index);
} else if (has_index) {
cJSON_AddNumberToObject(opts, "index", index_val);
} else {
cJSON_AddNumberToObject(opts, "index", 0);
}
cJSON_AddItemToArray(params, opts);
} else {
method = "nostr_get_public_key";
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
if (is_nostr_get_pubkey_structured) cJSON_AddStringToObject(opts, "format", "structured");
cJSON_AddItemToArray(params, opts);
}
} else if (strcmp(verb, "sign-event") == 0) {
method = "nostr_sign_event";
const char *event_json = arg1;
if (!event_json) {
event_json = read_stdin_line();
if (!event_json) {
fprintf(stderr, "error: no event JSON provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); free((char*)event_json); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
if (!arg1) free((char*)event_json);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "mine-event") == 0) {
method = "nostr_mine_event";
const char *event_json = arg1;
if (!event_json) {
event_json = read_stdin_line();
if (!event_json) {
fprintf(stderr, "error: no event JSON provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); free((char*)event_json); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
if (!arg1) free((char*)event_json);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
if (has_difficulty) cJSON_AddNumberToObject(opts, "difficulty", difficulty_val);
if (has_threads) cJSON_AddNumberToObject(opts, "threads", threads_val);
if (has_timeout_sec) cJSON_AddNumberToObject(opts, "timeout_sec", timeout_sec_val);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "nip04-encrypt") == 0) {
method = "nostr_nip04_encrypt";
if (!arg1) { fprintf(stderr, "error: nip04-encrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *peer = arg1;
const char *plaintext = arg2;
if (!plaintext) {
plaintext = read_stdin_line();
if (!plaintext) {
fprintf(stderr, "error: no plaintext provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg2) free((char*)plaintext); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(peer));
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext));
if (!arg2) free((char*)plaintext);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "nip04-decrypt") == 0) {
method = "nostr_nip04_decrypt";
if (!arg1) { fprintf(stderr, "error: nip04-decrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *peer = arg1;
const char *ciphertext = arg2;
if (!ciphertext) {
ciphertext = read_stdin_line();
if (!ciphertext) {
fprintf(stderr, "error: no ciphertext provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg2) free((char*)ciphertext); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(peer));
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
if (!arg2) free((char*)ciphertext);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "nip44-encrypt") == 0) {
method = "nostr_nip44_encrypt";
if (!arg1) { fprintf(stderr, "error: nip44-encrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *peer = arg1;
const char *plaintext = arg2;
if (!plaintext) {
plaintext = read_stdin_line();
if (!plaintext) {
fprintf(stderr, "error: no plaintext provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg2) free((char*)plaintext); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(peer));
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext));
if (!arg2) free((char*)plaintext);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "nip44-decrypt") == 0) {
method = "nostr_nip44_decrypt";
if (!arg1) { fprintf(stderr, "error: nip44-decrypt requires <peer-pubkey>\n"); goto cleanup; }
const char *peer = arg1;
const char *ciphertext = arg2;
if (!ciphertext) {
ciphertext = read_stdin_line();
if (!ciphertext) {
fprintf(stderr, "error: no ciphertext provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg2) free((char*)ciphertext); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(peer));
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
if (!arg2) free((char*)ciphertext);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
if (role) cJSON_AddStringToObject(opts, "role", role);
if (path) cJSON_AddStringToObject(opts, "role_path", path);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "sign") == 0) {
method = "sign";
if (!arg1) { fprintf(stderr, "error: sign requires <msg-hex>\n"); goto cleanup; }
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(arg1));
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "secp256k1");
if (has_alg_index) {
cJSON_AddNumberToObject(opts, "index", alg_index);
} else if (has_index) {
cJSON_AddNumberToObject(opts, "index", index_val);
} else {
cJSON_AddNumberToObject(opts, "index", 0);
}
if (scheme) cJSON_AddStringToObject(opts, "scheme", scheme);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "verify") == 0) {
method = "verify";
is_verify = 1;
if (!arg1 || !arg2) { fprintf(stderr, "error: verify requires <msg-hex> <sig-hex>\n"); goto cleanup; }
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(arg1));
cJSON_AddItemToArray(params, cJSON_CreateString(arg2));
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "secp256k1");
if (has_alg_index) {
cJSON_AddNumberToObject(opts, "index", alg_index);
} else if (has_index) {
cJSON_AddNumberToObject(opts, "index", index_val);
} else {
cJSON_AddNumberToObject(opts, "index", 0);
}
if (scheme) cJSON_AddStringToObject(opts, "scheme", scheme);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "derive") == 0) {
method = "derive";
const char *data = arg1;
if (!data) {
data = read_stdin_line();
if (!data) {
fprintf(stderr, "error: no data provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg1) free((char*)data); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(data));
if (!arg1) free((char*)data);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "secp256k1");
if (has_alg_index) {
cJSON_AddNumberToObject(opts, "index", alg_index);
} else if (has_index) {
cJSON_AddNumberToObject(opts, "index", index_val);
} else {
cJSON_AddNumberToObject(opts, "index", 0);
}
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "encapsulate") == 0) {
method = "encapsulate";
if (!arg1) { fprintf(stderr, "error: encapsulate requires <peer-pubkey-hex>\n"); goto cleanup; }
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(arg1));
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "ml-kem-768");
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "decapsulate") == 0) {
method = "decapsulate";
if (!arg1) { fprintf(stderr, "error: decapsulate requires <ciphertext-hex>\n"); goto cleanup; }
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(arg1));
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "ml-kem-768");
if (has_alg_index) {
cJSON_AddNumberToObject(opts, "index", alg_index);
} else if (has_index) {
cJSON_AddNumberToObject(opts, "index", index_val);
} else {
cJSON_AddNumberToObject(opts, "index", 0);
}
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "derive-shared-secret") == 0) {
method = "derive_shared_secret";
if (!arg1) { fprintf(stderr, "error: derive-shared-secret requires <peer-pubkey-hex>\n"); goto cleanup; }
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(arg1));
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", algorithm ? algorithm : "x25519");
if (has_alg_index) {
cJSON_AddNumberToObject(opts, "index", alg_index);
} else if (has_index) {
cJSON_AddNumberToObject(opts, "index", index_val);
} else {
cJSON_AddNumberToObject(opts, "index", 0);
}
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "encrypt") == 0) {
method = "encrypt";
const char *plaintext = arg1;
if (!plaintext) {
plaintext = read_stdin_line();
if (!plaintext) {
fprintf(stderr, "error: no plaintext provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg1) free((char*)plaintext); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext));
if (!arg1) free((char*)plaintext);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", "otp");
if (encoding) cJSON_AddStringToObject(opts, "encoding", encoding);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "decrypt") == 0) {
method = "decrypt";
const char *ciphertext = arg1;
if (!ciphertext) {
ciphertext = read_stdin_line();
if (!ciphertext) {
fprintf(stderr, "error: no ciphertext provided (pass as argument or pipe to stdin)\n");
goto cleanup;
}
}
params = cJSON_CreateArray();
if (!params) { fprintf(stderr, "error: out of memory\n"); if (!arg1) free((char*)ciphertext); goto cleanup; }
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
if (!arg1) free((char*)ciphertext);
cJSON *opts = cJSON_CreateObject();
if (!opts) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
cJSON_AddStringToObject(opts, "algorithm", "otp");
if (encoding) cJSON_AddStringToObject(opts, "encoding", encoding);
cJSON_AddItemToArray(params, opts);
} else if (strcmp(verb, "call") == 0) {
is_call_verb = 1;
if (!arg1) { fprintf(stderr, "error: call requires <method>\n"); goto cleanup; }
method = arg1;
/* Params: from remaining argv or stdin */
if (arg2) {
/* Use remaining argv as the params JSON */
/* Reconstruct the JSON array string from remaining args */
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 {
/* Read from stdin */
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;
}
}
} else {
fprintf(stderr, "error: unknown verb: %s\n", verb);
fprintf(stderr, "Try '%s --help' for usage.\n", prog);
goto cleanup;
}
/* ---- make the RPC call ---- */
if (nsigner_client_call(client, method, params, &result) != NOSTR_SUCCESS) {
fprintf(stderr, "error: %s\n", nsigner_client_last_error(client));
params = NULL; /* ownership transferred even on failure */
goto cleanup;
}
params = NULL; /* ownership transferred */
/* ---- print result ---- */
if (is_call_verb || strcmp(verb, "get-info") == 0) {
/* Raw JSON output for get_info and call */
if (result) {
char *json = cJSON_PrintUnformatted(result);
if (json) {
printf("%s\n", json);
free(json);
}
}
rc = 0;
} else {
int prc = print_result(result, is_verify);
if (is_verify) {
rc = (prc == 0 || prc == 1) ? prc : 2;
} else {
rc = 0;
}
}
cleanup:
if (rc != 0 && params) {
/* If we still own params and there was an error, free it.
* nsigner_client_call takes ownership on success, so we only
* free params here if we never called nsigner_client_call. */
cJSON_Delete(params);
}
cJSON_Delete(result);
if (client) {
nsigner_client_free(client); /* also closes/frees the transport */
} else if (transport) {
transport->close(transport);
}
nostr_cleanup();
return rc;
}