Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a16c4e1bf | ||
|
|
9afbb8fcbd | ||
|
|
fa98de12e4 | ||
|
|
e459e98beb |
+359
-451
@@ -5,6 +5,10 @@
|
||||
* (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...]
|
||||
*
|
||||
@@ -21,6 +25,7 @@
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -48,7 +53,7 @@ static void print_usage(FILE *fp, const char *prog) {
|
||||
" -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"
|
||||
" --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"
|
||||
@@ -181,74 +186,99 @@ static int parse_qube_service(const char *s, char **out_qube, char **out_service
|
||||
/* 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) {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
*out_transport = NULL;
|
||||
|
||||
if (tcp_arg) {
|
||||
if (!auth_privkey_hex) {
|
||||
fprintf(stderr, "error: --tcp requires --auth-privkey\n");
|
||||
return -1;
|
||||
}
|
||||
if (cJSON_IsBool(result)) {
|
||||
printf("%s\n", cJSON_IsTrue(result) ? "valid" : "invalid");
|
||||
return cJSON_IsTrue(result) ? 0 : 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;
|
||||
}
|
||||
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;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -335,10 +365,10 @@ int main(int argc, char **argv) {
|
||||
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; }
|
||||
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 base64 or hex\n"); return 2; }
|
||||
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; }
|
||||
@@ -384,14 +414,6 @@ int main(int argc, char **argv) {
|
||||
/* ---- 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 ||
|
||||
@@ -432,435 +454,347 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- transport setup ---- */
|
||||
/* ---- open transport ---- */
|
||||
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 (open_transport(socket_name, timeout_ms, tcp_arg, serial_arg, qrexec_arg,
|
||||
auth_privkey_hex, &transport) != 0) {
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
|
||||
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);
|
||||
/* ---- 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);
|
||||
goto cleanup;
|
||||
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");
|
||||
goto cleanup;
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
if (nsigner_client_set_auth(client, privkey, auth_label ? auth_label : "") != NOSTR_SUCCESS) {
|
||||
if (nostr_signer_nsigner_set_auth(signer, privkey, auth_label ? auth_label : "") != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "error: failed to set auth envelope\n");
|
||||
goto cleanup;
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- build params and call ---- */
|
||||
const char *method = NULL;
|
||||
int is_call_verb = 0;
|
||||
/* ---- 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) {
|
||||
method = "get_info";
|
||||
params = cJSON_CreateArray();
|
||||
if (!params) { fprintf(stderr, "error: out of memory\n"); goto cleanup; }
|
||||
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) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
} else {
|
||||
method = "nostr_get_public_key";
|
||||
params = cJSON_CreateArray();
|
||||
if (!params) { fprintf(stderr, "error: out of memory\n"); 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 (!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_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) {
|
||||
method = "nostr_sign_event";
|
||||
const char *event_json = arg1;
|
||||
char *event_buf = NULL;
|
||||
if (!event_json) {
|
||||
event_json = read_stdin_line();
|
||||
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;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
method = "nostr_mine_event";
|
||||
const char *event_json = arg1;
|
||||
char *event_buf = NULL;
|
||||
if (!event_json) {
|
||||
event_json = read_stdin_line();
|
||||
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;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
char *pt_buf = NULL;
|
||||
if (!plaintext) {
|
||||
plaintext = read_stdin_line();
|
||||
if (!plaintext) {
|
||||
fprintf(stderr, "error: no plaintext provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
pt_buf = read_stdin_line();
|
||||
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
|
||||
plaintext = pt_buf;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
char *ct_buf = NULL;
|
||||
if (!ciphertext) {
|
||||
ciphertext = read_stdin_line();
|
||||
if (!ciphertext) {
|
||||
fprintf(stderr, "error: no ciphertext provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ct_buf = read_stdin_line();
|
||||
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
|
||||
ciphertext = ct_buf;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
char *pt_buf = NULL;
|
||||
if (!plaintext) {
|
||||
plaintext = read_stdin_line();
|
||||
if (!plaintext) {
|
||||
fprintf(stderr, "error: no plaintext provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
pt_buf = read_stdin_line();
|
||||
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
|
||||
plaintext = pt_buf;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
char *ct_buf = NULL;
|
||||
if (!ciphertext) {
|
||||
ciphertext = read_stdin_line();
|
||||
if (!ciphertext) {
|
||||
fprintf(stderr, "error: no ciphertext provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ct_buf = read_stdin_line();
|
||||
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
|
||||
ciphertext = ct_buf;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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);
|
||||
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) {
|
||||
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);
|
||||
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) {
|
||||
method = "derive";
|
||||
const char *data = arg1;
|
||||
char *data_buf = NULL;
|
||||
if (!data) {
|
||||
data = read_stdin_line();
|
||||
if (!data) {
|
||||
fprintf(stderr, "error: no data provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
cJSON_Delete(dresult);
|
||||
rc = 0;
|
||||
} else {
|
||||
cJSON_AddNumberToObject(opts, "index", 0);
|
||||
/* 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;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
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);
|
||||
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) {
|
||||
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);
|
||||
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) {
|
||||
method = "encrypt";
|
||||
const char *plaintext = arg1;
|
||||
char *pt_buf = NULL;
|
||||
if (!plaintext) {
|
||||
plaintext = read_stdin_line();
|
||||
if (!plaintext) {
|
||||
fprintf(stderr, "error: no plaintext provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
pt_buf = read_stdin_line();
|
||||
if (!pt_buf) { fprintf(stderr, "error: no plaintext provided\n"); goto cleanup; }
|
||||
plaintext = pt_buf;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
method = "decrypt";
|
||||
const char *ciphertext = arg1;
|
||||
char *ct_buf = NULL;
|
||||
if (!ciphertext) {
|
||||
ciphertext = read_stdin_line();
|
||||
if (!ciphertext) {
|
||||
fprintf(stderr, "error: no ciphertext provided (pass as argument or pipe to stdin)\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ct_buf = read_stdin_line();
|
||||
if (!ct_buf) { fprintf(stderr, "error: no ciphertext provided\n"); goto cleanup; }
|
||||
ciphertext = ct_buf;
|
||||
}
|
||||
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);
|
||||
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) {
|
||||
is_call_verb = 1;
|
||||
/* Raw passthrough using the low-level client (shared with signer). */
|
||||
if (!arg1) { fprintf(stderr, "error: call requires <method>\n"); goto cleanup; }
|
||||
method = arg1;
|
||||
/* Params: from remaining argv or stdin */
|
||||
const char *method = arg1;
|
||||
|
||||
cJSON *params = NULL;
|
||||
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;
|
||||
@@ -879,7 +813,6 @@ int main(int argc, char **argv) {
|
||||
goto cleanup;
|
||||
}
|
||||
} else {
|
||||
/* Read from stdin */
|
||||
char *line = read_stdin_line();
|
||||
if (!line) {
|
||||
fprintf(stderr, "error: no params JSON on stdin\n");
|
||||
@@ -892,53 +825,28 @@ int main(int argc, char **argv) {
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
if (result_str) free(result_str);
|
||||
if (result_obj) cJSON_Delete(result_obj);
|
||||
if (signer) nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# Audit: n_signer Breaking Changes vs Client Repos
|
||||
|
||||
## 1. The breaking changes made to n_signer
|
||||
|
||||
Three changes on the n_signer wire protocol are breaking for every existing
|
||||
client. All three are landed in `src/` and documented in `README.md` §4.
|
||||
|
||||
### 1.1 Verb renames (legacy names removed)
|
||||
|
||||
Source: [`plans/legacy_verb_aliases.md`](legacy_verb_aliases.md) — COMPLETED.
|
||||
|
||||
| Old wire verb | New wire verb |
|
||||
|--------------------|----------------------------|
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `mine_event` | `nostr_mine_event` |
|
||||
| `nip04_encrypt` | `nostr_nip04_encrypt` |
|
||||
| `nip04_decrypt` | `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` | `nostr_nip44_encrypt` |
|
||||
| `nip44_decrypt` | `nostr_nip44_decrypt` |
|
||||
| `get_public_key` (role branch) | `nostr_get_public_key` |
|
||||
|
||||
The role-based `get_public_key` was split: algorithm-based stays
|
||||
`get_public_key`; Nostr-protocol key selection is now `nostr_get_public_key`.
|
||||
The old alias names are **gone** — no shim, no fallthrough.
|
||||
|
||||
### 1.2 Selector model rewrite (nostr_index / index removed for nostr verbs)
|
||||
|
||||
Source: [`plans/role_path_authorization.md`](role_path_authorization.md).
|
||||
|
||||
- `nostr_index` selector → **removed**, rejected with error `2006
|
||||
nostr_index_deprecated` (see [`src/dispatcher.c`](../src/dispatcher.c:1815)).
|
||||
- `index` on `nostr_*` verbs → **removed**, rejected with `2007
|
||||
index_deprecated`.
|
||||
- The **only** accepted selector for `nostr_*` verbs is now `{"role":"<name>",
|
||||
"role_path":"<full-path>"}` sent **together**. Either field alone is
|
||||
rejected: `2008 role_required` / `2009 path_required`
|
||||
([`README.md`](../README.md) §4.6).
|
||||
- No backward compatibility. `--nostr-index` / `--index` on the client CLI are
|
||||
removed; replaced by `--role` + `--path`.
|
||||
|
||||
### 1.3 OTP encoding values changed
|
||||
|
||||
`encrypt` / `decrypt` (algorithm `otp`) now take `encoding` =
|
||||
`"ascii"` (ASCII-armored, default) or `"binary"` (base64 raw `.otp` blob)
|
||||
([`src/dispatcher.c`](../src/dispatcher.c:1452), [`src/otp_pad.c`](../src/otp_pad.c:347)).
|
||||
|
||||
Note: [`client/n_signer_client.c`](../client/n_signer_client.c:51) help text
|
||||
still advertises `--encoding <base64|hex>` — that is a **stale doc string**
|
||||
inside n_signer's own client and should be fixed to `ascii|binary`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Are these reflected in the nostr_core_lib repo? — NO
|
||||
|
||||
`nostr_core_lib` is the shared client library that every C-based n_signer
|
||||
client links against. It is **out of date** and will fail against current
|
||||
n_signer. Specific gaps:
|
||||
|
||||
### 2.1 Still emits the removed `nostr_index` selector
|
||||
|
||||
[`nostr_core_lib/nostr_core/nostr_signer.c`](../../nostr_core_lib/nostr_core/nostr_signer.c:309)
|
||||
`signer_remote_params_with_selector()` emits `{"nostr_index":N}` when set
|
||||
(lines 320–327). n_signer now rejects this with `2006 nostr_index_deprecated`.
|
||||
|
||||
The public API
|
||||
[`nostr_signer_nsigner_set_nostr_index()`](../../nostr_core_lib/nostr_core/nostr_signer.c:858)
|
||||
still exists and is the documented way to select a key — it is now a dead end.
|
||||
|
||||
### 2.2 Sends `role` without `role_path`
|
||||
|
||||
When `nostr_index` is not set, the same helper emits only `{"role":"..."}`
|
||||
(line 341) with no `role_path`. n_signer now requires both and rejects
|
||||
role-only with `2009 path_required`.
|
||||
|
||||
The `nostr_signer_nsigner_*` factory constructors
|
||||
([`nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h:51)) take a
|
||||
single `const char* role` parameter — there is no way to pass a `role_path`
|
||||
through the high-level API at all.
|
||||
|
||||
### 2.3 `derive` (HMAC) path is half-broken
|
||||
|
||||
[`nostr_signer.c`](../../nostr_core_lib/nostr_core/nostr_signer.c:560) builds
|
||||
`{"algorithm":"secp256k1","index":N}` for the `derive` verb. The
|
||||
algorithm-based `derive` verb still accepts `index`, so the `nostr_index` branch
|
||||
works. But the `role`-only branch (line 563) sends `{"role":"..."}` with no
|
||||
`index` — `derive` requires `index` and will reject it.
|
||||
|
||||
### 2.4 Documentation is stale
|
||||
|
||||
[`NSIGNER_INTEGRATION.md`](../../nostr_core_lib/nostr_core/NSIGNER_INTEGRATION.md:123)
|
||||
still tells integrators to use `nostr_index` and `role`-only selectors, and
|
||||
[`plans/nostr_core_lib_client_updates.md`](../../nostr_core_lib/plans/nostr_core_lib_client_updates.md)
|
||||
proposes `nostr_index` support as the chosen design — both predate the
|
||||
selector rewrite.
|
||||
|
||||
### 2.5 What needs to change in nostr_core_lib
|
||||
|
||||
1. Replace the `role`-only + `nostr_index` selector model with a combined
|
||||
`role` + `role_path` selector. Concretely: change the `nostr_signer_nsigner_*`
|
||||
constructors (or add new ones / a selector struct) to accept both a role
|
||||
name and a full path.
|
||||
2. Remove `nostr_signer_nsigner_set_nostr_index` (or repurpose it to set
|
||||
`role` + `role_path` from an index by expanding the NIP-06 template
|
||||
`m/44'/1237'/N'/0/0` client-side).
|
||||
3. Update `signer_remote_params_with_selector` to always emit both `role` and
|
||||
`role_path`.
|
||||
4. Fix the `derive` remote path to always include `index`.
|
||||
5. Update `NSIGNER_INTEGRATION.md`, `nostr_core_lib_client_updates.md`, and
|
||||
`tests/nsigner_client_test.c` (which sends `nostr_get_public_key` with a
|
||||
`nostr_index` selector at line 297).
|
||||
|
||||
---
|
||||
|
||||
## 3. Repos in ~/lt/ that need client edits
|
||||
|
||||
### Tier 1 — Direct n_signer wire clients (BROKEN now)
|
||||
|
||||
These talk the n_signer JSON-RPC protocol directly and will fail against
|
||||
current n_signer:
|
||||
|
||||
| Repo | Files | Problem |
|
||||
|------|-------|---------|
|
||||
| **nostr_core_lib** | `nostr_core/nostr_signer.c`, `nostr_signer.h`, `nsigner_client.c`, `NSIGNER_INTEGRATION.md`, `tests/nsigner_client_test.c`, `examples/note_poster.c` | Emits removed `nostr_index`; sends `role` without `role_path`. Shared lib — fixing this fixes all C clients that link it. |
|
||||
| **nostr_terminal** | `src/nsigner_client.c`, `include/nsigner_client.h`, `src/signer.c`, `src/menu_login.c`, `src/menu_profile.c`, `plans/n_signer_integration.md` | Has its own hand-rolled `nsigner_client` that sends `{"nostr_index":N}` ([`nsigner_client.c`](../../nostr_terminal/src/nsigner_client.c:617)). Selector struct is `has_nostr_index`/`nostr_index`/`role` with no `role_path`. Login menu prompts for "index" only. |
|
||||
| **sovereign_browser** | `src/login_dialog.c`, `src/agent_login.c`, `src/key_store.c`, `src/key_store.h` | Uses `nostr_signer_nsigner_*` from nostr_core_lib + `nostr_signer_nsigner_set_nostr_index`. UI has a nostr_index spin button. Breaks via the lib, and the UI needs a role+path input. |
|
||||
| **laantungir_website** | `scripts/publish_nostr.js`, `scripts/get_nsigner_pubkey.js` | Raw JSON-RPC over qrexec sending `{"nostr_index": N}` ([`publish_nostr.js`](../../laantungir_website/scripts/publish_nostr.js:103)). Will get `2006`. |
|
||||
|
||||
### Tier 2 — Indirect (breaks once Tier 1 lib is fixed, or uses nostr_core_lib local signing only)
|
||||
|
||||
| Repo | Status | Action |
|
||||
|------|--------|--------|
|
||||
| **n_signer** (this repo) | `client/n_signer_client.c` help text says `--encoding <base64\|hex>` but server wants `ascii\|binary`; the client itself already uses `--role`+`--path` correctly per [`role_path_authorization.md`](role_path_authorization.md). | Fix the stale `--encoding` help string. |
|
||||
|
||||
### Not affected (use local nostr_core_lib signing, not n_signer remote)
|
||||
|
||||
These call `nostr_create_and_sign_event` / `nostr_signer_local` with a local
|
||||
private key — they do not speak the n_signer wire protocol and are unaffected:
|
||||
|
||||
- `open_wire` (local `sign_event` helper, not n_signer RPC)
|
||||
- `raspberry_pi_zero_nostr` (local `nostr_create_and_sign_event`)
|
||||
- `esp32_playground` (local `nostr_create_and_sign_event`)
|
||||
|
||||
### Not affected (NIP-46 to arbitrary remote signers, not n_signer)
|
||||
|
||||
These use NIP-46 method names (`sign_event`, `nip04_encrypt`, …) per the NIP-46
|
||||
spec, targeting generic remote signers / browser extensions — not n_signer's
|
||||
renamed verbs. No change needed unless they specifically add an n_signer
|
||||
backend:
|
||||
|
||||
- `primal-web-app` (`src/lib/nip46/nip46.ts`)
|
||||
- `super_ball` (`web/nostr.bundle.js`)
|
||||
- `nips` (spec docs)
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommended remediation order
|
||||
|
||||
1. **nostr_core_lib** first — it is the shared dependency. Introduce a
|
||||
`role` + `role_path` selector (struct or new constructors), remove
|
||||
`nostr_index` emission, fix `derive`, update tests + integration doc.
|
||||
2. **sovereign_browser** — update login UI to collect role + path instead of
|
||||
index; switch to the new nostr_core_lib API.
|
||||
3. **nostr_terminal** — rewrite its hand-rolled `nsigner_client` selector to
|
||||
`role` + `role_path`; update login/profile menus and the integration plan.
|
||||
4. **laantungir_website** — switch the two JS scripts from `nostr_index` to
|
||||
`role` + `role_path`.
|
||||
5. **n_signer** — fix the stale `--encoding` help string in
|
||||
`client/n_signer_client.c`.
|
||||
|
||||
A Mermaid overview of the dependency order:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
NS[n_signer wire changes] --> NCL[nostr_core_lib]
|
||||
NCL --> SB[sovereign_browser]
|
||||
NCL --> NT[nostr_terminal]
|
||||
NS --> LW[laantungir_website]
|
||||
NS --> NSC[n_signer client help text]
|
||||
```
|
||||
@@ -0,0 +1,197 @@
|
||||
# Analysis: Does nostr_core_lib Fully Cover the n_signer Client Verb Surface?
|
||||
|
||||
## Question
|
||||
|
||||
> When we wrote `n_signer_client` in this project, did we utilize
|
||||
> `nostr_core_lib` to the fullest? If a client wants to interface with
|
||||
> nsigner, they can use the CLI, or write C utilizing the functions in
|
||||
> `nostr_core_lib`. Did we fully put into nostr_core_lib the functionality
|
||||
> of our client? I have a suspicion we wrote the client and didn't add back
|
||||
> into nostr_core_lib.
|
||||
|
||||
## Answer: Your suspicion is correct — the library covers less than half the verb surface.
|
||||
|
||||
The CLI ([`client/n_signer_client.c`](../client/n_signer_client.c)) exposes
|
||||
**16 verbs**. The `nostr_core_lib` high-level `nostr_signer_t` API
|
||||
([`nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h)) exposes
|
||||
only **6** of them. The CLI hand-builds cJSON params and calls the low-level
|
||||
`nsigner_client_call()` for the other 10 verbs — none of which have a
|
||||
high-level library wrapper.
|
||||
|
||||
## Verb-by-verb coverage
|
||||
|
||||
| n_signer wire verb | CLI verb | `nostr_signer_t` high-level API | Status |
|
||||
|--------------------|----------|---------------------------------|--------|
|
||||
| `get_info` | `get-info` | — | **Missing** |
|
||||
| `get_public_key` (algorithm) | `get-public-key -a <alg>` | — | **Missing** |
|
||||
| `nostr_get_public_key` | `get-public-key --role --path` | `nostr_signer_get_public_key()` | Covered |
|
||||
| `nostr_sign_event` | `sign-event` | `nostr_signer_sign_event()` | Covered |
|
||||
| `nostr_mine_event` | `mine-event` | — | **Missing** |
|
||||
| `nostr_nip04_encrypt` | `nip04-encrypt` | `nostr_signer_nip04_encrypt()` | Covered |
|
||||
| `nostr_nip04_decrypt` | `nip04-decrypt` | `nostr_signer_nip04_decrypt()` | Covered |
|
||||
| `nostr_nip44_encrypt` | `nip44-encrypt` | `nostr_signer_nip44_encrypt()` | Covered |
|
||||
| `nostr_nip44_decrypt` | `nip44-decrypt` | `nostr_signer_nip44_decrypt()` | Covered |
|
||||
| `sign` | `sign` | — | **Missing** |
|
||||
| `verify` | `verify` | — | **Missing** |
|
||||
| `derive` | `derive` | `nostr_signer_derive_hmac()` | **Partial** (lib wraps it as HMAC-only, hardcodes `algorithm:"secp256k1"`; the raw `derive` verb is not exposed) |
|
||||
| `encapsulate` | `encapsulate` | — | **Missing** |
|
||||
| `decapsulate` | `decapsulate` | — | **Missing** |
|
||||
| `derive_shared_secret` | `derive-shared-secret` | — | **Missing** |
|
||||
| `encrypt` (OTP) | `encrypt` | — | **Missing** |
|
||||
| `decrypt` (OTP) | `decrypt` | — | **Missing** |
|
||||
| (raw passthrough) | `call <method>` | `nsigner_client_call()` (low-level) | Covered at low level |
|
||||
|
||||
**Score: 6 covered, 1 partial, 10 missing.**
|
||||
|
||||
## What the CLI does that the library doesn't
|
||||
|
||||
The CLI is essentially a thin argv-to-JSON-RPC mapper. For each verb it:
|
||||
1. Builds a `cJSON` params array with the positional args + options object.
|
||||
2. Calls `nsigner_client_call(client, method, params, &result)`.
|
||||
3. Prints the result.
|
||||
|
||||
This is exactly the kind of per-verb glue that belongs in the library, not
|
||||
duplicated in every client. Today a C client that wants to call `sign` with
|
||||
`ed25519` must either:
|
||||
- drop down to the low-level `nsigner_client_call` and hand-build cJSON (what
|
||||
the CLI does), or
|
||||
- not use the library for that verb at all.
|
||||
|
||||
## Two layers in nostr_core_lib today
|
||||
|
||||
The library has two layers, and the gap is in the **high-level** layer:
|
||||
|
||||
1. **Low-level** ([`nsigner_client.h`](../../nostr_core_lib/nostr_core/nsigner_client.h)):
|
||||
`nsigner_client_call(client, method, params, &result)` — generic
|
||||
JSON-RPC. This covers *everything* but forces the caller to build cJSON
|
||||
params by hand and parse cJSON results by hand. The CLI uses this layer
|
||||
exclusively.
|
||||
|
||||
2. **High-level** ([`nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h)):
|
||||
`nostr_signer_t` with typed verbs that take C strings/bytes and return
|
||||
C strings/bytes. This is the layer a C client *wants* to use. It only
|
||||
covers the 6 Nostr verbs + `derive_hmac`.
|
||||
|
||||
## What's missing and where it should go
|
||||
|
||||
The high-level `nostr_signer_t` API should gain typed wrappers for the
|
||||
algorithm-based verbs. Proposed additions (all on `nostr_signer_t`, remote
|
||||
backend routes to `nsigner_client_call` with the right method+params):
|
||||
|
||||
### Metadata
|
||||
```c
|
||||
int nostr_signer_get_info(nostr_signer_t* signer, cJSON** info_out);
|
||||
```
|
||||
|
||||
### Algorithm-based key/sign/verify (the `algorithm` + `index` selector)
|
||||
```c
|
||||
int nostr_signer_get_public_key_alg(nostr_signer_t* signer,
|
||||
const char* algorithm, int index,
|
||||
char** pubkey_hex_out);
|
||||
|
||||
int nostr_signer_sign(nostr_signer_t* signer,
|
||||
const char* algorithm, int index,
|
||||
const char* scheme, /* "schnorr"|"ecdsa"|NULL */
|
||||
const unsigned char* msg, size_t msg_len,
|
||||
char** sig_hex_out);
|
||||
|
||||
int nostr_signer_verify(nostr_signer_t* signer,
|
||||
const char* algorithm, int index,
|
||||
const char* scheme,
|
||||
const unsigned char* msg, size_t msg_len,
|
||||
const unsigned char* sig, size_t sig_len,
|
||||
int* valid_out);
|
||||
```
|
||||
|
||||
### Post-quantum KEM
|
||||
```c
|
||||
int nostr_signer_encapsulate(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
char** ciphertext_hex_out,
|
||||
char** shared_secret_hex_out);
|
||||
|
||||
int nostr_signer_decapsulate(nostr_signer_t* signer, int index,
|
||||
const char* ciphertext_hex,
|
||||
char** shared_secret_hex_out);
|
||||
```
|
||||
|
||||
### X25519 key agreement
|
||||
```c
|
||||
int nostr_signer_derive_shared_secret(nostr_signer_t* signer, int index,
|
||||
const char* peer_pubkey_hex,
|
||||
char** shared_secret_hex_out);
|
||||
```
|
||||
|
||||
### OTP one-time pad
|
||||
```c
|
||||
int nostr_signer_otp_encrypt(nostr_signer_t* signer,
|
||||
const char* plaintext_b64,
|
||||
const char* encoding, /* "ascii"|"binary"|NULL */
|
||||
char** ciphertext_out);
|
||||
|
||||
int nostr_signer_otp_decrypt(nostr_signer_t* signer,
|
||||
const char* ciphertext,
|
||||
const char* encoding,
|
||||
char** plaintext_out);
|
||||
```
|
||||
|
||||
### Nostr mine-event (POW)
|
||||
```c
|
||||
int nostr_signer_mine_event(nostr_signer_t* signer,
|
||||
const cJSON* unsigned_event,
|
||||
int difficulty, int timeout_sec, int threads,
|
||||
cJSON** signed_event_out);
|
||||
```
|
||||
|
||||
### Raw derive (the lib's `derive_hmac` is a specialization; expose the general verb)
|
||||
The existing `nostr_signer_derive_hmac` is fine as a convenience; no change
|
||||
needed, but the raw `derive` verb is already reachable through it.
|
||||
|
||||
## Impact on the CLI
|
||||
|
||||
If these wrappers are added to `nostr_core_lib`, the CLI
|
||||
([`client/n_signer_client.c`](../client/n_signer_client.c)) shrinks
|
||||
dramatically. Today it is ~945 lines, most of which is the per-verb
|
||||
`cJSON_CreateArray` / `cJSON_AddStringToObject` / `cJSON_AddNumberToObject`
|
||||
boilerplate. With the wrappers, each verb handler becomes a 3–5 line call to
|
||||
the library + `print_result`. The CLI becomes what you envisioned: mostly
|
||||
interface code (argv parsing + result printing) with the real logic in the
|
||||
library.
|
||||
|
||||
## Impact on other clients
|
||||
|
||||
Every C client that currently hand-builds JSON-RPC for the missing verbs
|
||||
(`nostr_terminal`'s `nsigner_client.c`, `sovereign_browser`, future embedded
|
||||
clients) would get typed wrappers for free and could stop hand-rolling cJSON.
|
||||
|
||||
## Recommendation
|
||||
|
||||
1. **Add the 10 missing high-level wrappers** to `nostr_signer.h` /
|
||||
`nostr_signer.c` in `nostr_core_lib` (remote backend only; the local
|
||||
backend can return `NOSTR_ERROR_NOT_SUPPORTED` for the algorithm-based
|
||||
verbs that are inherently signer-side).
|
||||
2. **Refactor `n_signer_client.c`** to call the wrappers instead of
|
||||
hand-building cJSON. This validates the API (the CLI becomes the first
|
||||
consumer) and shrinks the client to mostly argv parsing + printing.
|
||||
3. **Add tests** for the new wrappers in
|
||||
[`nostr_core_lib/tests/nsigner_client_test.c`](../../nostr_core_lib/tests/nsigner_client_test.c)
|
||||
using the mock-transport pattern already there.
|
||||
|
||||
A Mermaid view of the target architecture:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CLI[n_signer_client CLI<br/>argv parse + print]
|
||||
LIB[nostr_core_lib<br/>nostr_signer_t high-level<br/>16 typed verbs]
|
||||
LOW[nostr_core_lib<br/>nsigner_client_call<br/>low-level JSON-RPC]
|
||||
NS[n_signer process<br/>wire protocol]
|
||||
|
||||
CLI --> LIB
|
||||
LIB --> LOW
|
||||
LOW -->|framed JSON-RPC| NS
|
||||
|
||||
OtherC[other C clients<br/>sovereign_browser<br/>nostr_terminal] --> LIB
|
||||
```
|
||||
|
||||
Today the `CLI --> LOW` arrow bypasses `LIB` for 10 of 16 verbs. The goal is
|
||||
to make `CLI --> LIB` the only path.
|
||||
+207
-34
@@ -813,8 +813,8 @@ int socket_name_random(char *out, size_t out_len);
|
||||
/* Version information (auto-updated by build/version tooling) */
|
||||
#define NSIGNER_VERSION_MAJOR 0
|
||||
#define NSIGNER_VERSION_MINOR 1
|
||||
#define NSIGNER_VERSION_PATCH 18
|
||||
#define NSIGNER_VERSION "v0.1.18"
|
||||
#define NSIGNER_VERSION_PATCH 21
|
||||
#define NSIGNER_VERSION "v0.1.21"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
@@ -1349,7 +1349,8 @@ static void print_usage(const char *program_name) {
|
||||
tui_print("nsigner - single-binary signer program");
|
||||
tui_print("Usage:");
|
||||
tui_print(" %s [--socket-name|--name|-n <name>] [--listen|-l <unix|stdio|qrexec|tcp:HOST:PORT>]", program_name);
|
||||
tui_print(" [--preapprove|-p <SPEC>]... [--auth|-a <off|optional|required>]");
|
||||
tui_print(" [--preapprove|-p <SPEC>]... [--register-role <SPEC>]...");
|
||||
tui_print(" [--auth|-a <off|optional|required>]");
|
||||
tui_print(" [--mnemonic-stdin|--mnemonic-fd <N>] [--allow-all|-A]");
|
||||
tui_print(" [--bridge-source-trusted]");
|
||||
tui_print(" Run signer server (unix mode has TUI)");
|
||||
@@ -1365,6 +1366,11 @@ static void print_usage(const char *program_name) {
|
||||
tui_print(" --preapprove, -p SPEC");
|
||||
tui_print(" Pre-approve a caller for a role (repeatable)");
|
||||
tui_print(" SPEC: caller=<id>,role=<name> or caller=<id>,nostr_index=<n>");
|
||||
tui_print(" --register-role SPEC");
|
||||
tui_print(" Register a named path-role non-interactively (repeatable)");
|
||||
tui_print(" SPEC: <name>:<curve>:<path-template>");
|
||||
tui_print(" e.g. nostr_range:secp256k1:m/44'/1237'/*'/0/0");
|
||||
tui_print(" Suppresses the default 'main' role in non-TTY mode");
|
||||
tui_print(" --auth, -a MODE Auth envelope policy per listener: off|optional|required");
|
||||
tui_print(" --mnemonic-stdin Read mnemonic from stdin (one line) at startup");
|
||||
tui_print(" --mnemonic-fd N Read mnemonic from inherited fd N (one line) at startup");
|
||||
@@ -1434,9 +1440,9 @@ static int list_sockets_main(void) {
|
||||
}
|
||||
|
||||
while (fgets(line, sizeof(line), fp) != NULL) {
|
||||
char name_with_at[SERVER_SOCKET_NAME_MAX + 1];
|
||||
if (extract_nsigner_socket_from_proc_line(line, name_with_at, sizeof(name_with_at), NULL, 0) == 0) {
|
||||
printf("%s\n", name_with_at);
|
||||
char name_no_at[SERVER_SOCKET_NAME_MAX + 1];
|
||||
if (extract_nsigner_socket_from_proc_line(line, NULL, 0, name_no_at, sizeof(name_no_at)) == 0) {
|
||||
printf("%s\n", name_no_at);
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
@@ -1521,7 +1527,7 @@ static int client_main(int argc, char *argv[], const char *socket_name, int sock
|
||||
|
||||
fd = connect_abstract_socket(socket_name);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "Failed to connect to @%s: %s\n", socket_name, strerror(errno));
|
||||
fprintf(stderr, "Failed to connect to %s: %s\n", socket_name, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1614,7 +1620,7 @@ static int bridge_main(int argc, char *argv[], const char *socket_name, int sock
|
||||
/* Connect to the persistent signer's abstract unix socket */
|
||||
fd = connect_abstract_socket(target_socket);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "bridge: cannot connect to @%s: %s\n", target_socket, strerror(errno));
|
||||
fprintf(stderr, "bridge: cannot connect to %s: %s\n", target_socket, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2667,6 +2673,145 @@ static role_purpose_t purpose_from_path(const char *path) {
|
||||
return PURPOSE_NOSTR; /* default */
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a --register-role spec of the form:
|
||||
* <name>:<curve>:<path-template>
|
||||
* and register it into the role table. The curve field may be empty
|
||||
* (e.g. "name::path") in which case the curve is auto-detected from the
|
||||
* path prefix via purpose_from_path + the default curve for that purpose.
|
||||
*
|
||||
* The path-template uses the same syntax as the TUI wizard:
|
||||
* - Wildcard: * or *' (any non-negative integer)
|
||||
* - Range: 0-99 or 0-99' (inclusive range)
|
||||
* - Set: 1+3+5 or 1+3-5+10
|
||||
* - Fixed: a literal path with no variable segment
|
||||
*
|
||||
* Returns 0 on success, -1 on parse error.
|
||||
*/
|
||||
static int register_role_from_spec(role_table_t *role_table, const char *spec) {
|
||||
char buf[512];
|
||||
char *name, *curve_str, *path_token;
|
||||
role_curve_t curve;
|
||||
role_purpose_t purpose;
|
||||
char template[ROLE_PATH_MAX];
|
||||
int range_lo, range_hi;
|
||||
int allowed_indices[64];
|
||||
int allowed_count = 0;
|
||||
role_entry_t new_role;
|
||||
|
||||
if (role_table == NULL || spec == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
strncpy(buf, spec, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
|
||||
/* Split on ':' — name:curve:path-template */
|
||||
name = buf;
|
||||
curve_str = strchr(buf, ':');
|
||||
if (curve_str == NULL) {
|
||||
fprintf(stderr, "--register-role: missing ':' separator in '%s'\n", spec);
|
||||
return -1;
|
||||
}
|
||||
*curve_str = '\0';
|
||||
curve_str++;
|
||||
path_token = strchr(curve_str, ':');
|
||||
if (path_token == NULL) {
|
||||
fprintf(stderr, "--register-role: missing path template in '%s' (expected name:curve:path)\n", spec);
|
||||
return -1;
|
||||
}
|
||||
*path_token = '\0';
|
||||
path_token++;
|
||||
|
||||
if (name[0] == '\0') {
|
||||
fprintf(stderr, "--register-role: empty role name in '%s'\n", spec);
|
||||
return -1;
|
||||
}
|
||||
if (path_token[0] == '\0') {
|
||||
fprintf(stderr, "--register-role: empty path template in '%s'\n", spec);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Resolve curve: empty string → auto-detect from path */
|
||||
if (curve_str[0] == '\0') {
|
||||
purpose = purpose_from_path(path_token);
|
||||
/* Default curve per purpose */
|
||||
switch (purpose) {
|
||||
case PURPOSE_SSH: curve = CURVE_ED25519; break;
|
||||
case PURPOSE_AGE: curve = CURVE_X25519; break;
|
||||
case PURPOSE_PQ_SIG:
|
||||
/* 102003 → ml-dsa-65, 102004 → slh-dsa-128s */
|
||||
if (strncmp(path_token, "m/44'/102004'", 13) == 0) {
|
||||
curve = CURVE_SLH_DSA_128S;
|
||||
} else {
|
||||
curve = CURVE_ML_DSA_65;
|
||||
}
|
||||
break;
|
||||
case PURPOSE_PQ_KEM: curve = CURVE_ML_KEM_768; break;
|
||||
default: curve = CURVE_SECP256K1; break;
|
||||
}
|
||||
} else {
|
||||
curve = role_curve_from_str(curve_str);
|
||||
if (curve == CURVE_UNKNOWN) {
|
||||
fprintf(stderr, "--register-role: unknown curve '%s' in '%s'\n", curve_str, spec);
|
||||
return -1;
|
||||
}
|
||||
purpose = purpose_from_path(path_token);
|
||||
}
|
||||
|
||||
/* Parse the path template (wildcard/range/set → %d) */
|
||||
if (parse_path_template_for_role(path_token, template, sizeof(template),
|
||||
&range_lo, &range_hi,
|
||||
allowed_indices, 64, &allowed_count) != 0) {
|
||||
fprintf(stderr, "--register-role: invalid path template '%s'\n", path_token);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Validate purpose+curve combination */
|
||||
if (crypto_alg_from_role(curve, purpose) == CRYPTO_ALG_UNKNOWN) {
|
||||
fprintf(stderr, "--register-role: curve '%s' is not valid for path '%s'\n",
|
||||
role_curve_to_str(curve), path_token);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Register the role (no approval required in non-interactive mode) */
|
||||
memset(&new_role, 0, sizeof(new_role));
|
||||
strncpy(new_role.name, name, sizeof(new_role.name) - 1);
|
||||
new_role.name[sizeof(new_role.name) - 1] = '\0';
|
||||
strncpy(new_role.purpose_str, role_purpose_to_str(purpose), sizeof(new_role.purpose_str) - 1);
|
||||
new_role.purpose_str[sizeof(new_role.purpose_str) - 1] = '\0';
|
||||
strncpy(new_role.curve_str, role_curve_to_str(curve), sizeof(new_role.curve_str) - 1);
|
||||
new_role.curve_str[sizeof(new_role.curve_str) - 1] = '\0';
|
||||
new_role.purpose = purpose;
|
||||
new_role.curve = curve;
|
||||
new_role.selector_type = SELECTOR_ROLE_PATH;
|
||||
strncpy(new_role.role_path, template, sizeof(new_role.role_path) - 1);
|
||||
new_role.role_path[sizeof(new_role.role_path) - 1] = '\0';
|
||||
new_role.nostr_index = -1;
|
||||
new_role.path_range_lo = range_lo;
|
||||
new_role.path_range_hi = range_hi;
|
||||
new_role.path_default_index = -1;
|
||||
new_role.requires_approval = 0; /* non-interactive: no TUI approval */
|
||||
if (allowed_count > 0) {
|
||||
int copy_n = allowed_count;
|
||||
if (copy_n > (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0]))) {
|
||||
copy_n = (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0]));
|
||||
}
|
||||
memcpy(new_role.path_allowed_indices, allowed_indices, (size_t)copy_n * sizeof(int));
|
||||
new_role.path_allowed_count = copy_n;
|
||||
} else {
|
||||
new_role.path_allowed_count = 0;
|
||||
}
|
||||
new_role.derived = 0;
|
||||
|
||||
if (role_table_add(role_table, &new_role) != 0) {
|
||||
fprintf(stderr, "--register-role: failed to register role '%s' (table full or duplicate name)\n", name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int prompt_load_mnemonic_tui(mnemonic_state_t *mnemonic) {
|
||||
char phrase[MNEMONIC_MAX_LEN];
|
||||
@@ -3405,6 +3550,8 @@ int main(int argc, char *argv[]) {
|
||||
int argi = 1;
|
||||
const char *preapprove_specs[POLICY_MAX_ENTRIES];
|
||||
int preapprove_count = 0;
|
||||
const char *register_role_specs[ROLE_TABLE_MAX_ENTRIES];
|
||||
int register_role_count = 0;
|
||||
int auth_mode = NSIGNER_AUTH_OFF;
|
||||
int auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS;
|
||||
int allow_all = 0;
|
||||
@@ -3483,6 +3630,19 @@ int main(int argc, char *argv[]) {
|
||||
argi += 2;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(argv[argi], "--register-role") == 0) {
|
||||
if (argi + 1 >= argc) {
|
||||
fprintf(stderr, "Missing value for %s\n", argv[argi]);
|
||||
return 1;
|
||||
}
|
||||
if (register_role_count >= ROLE_TABLE_MAX_ENTRIES) {
|
||||
fprintf(stderr, "Too many --register-role entries (max %d)\n", ROLE_TABLE_MAX_ENTRIES);
|
||||
return 1;
|
||||
}
|
||||
register_role_specs[register_role_count++] = argv[argi + 1];
|
||||
argi += 2;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(argv[argi], "--mnemonic-stdin") == 0) {
|
||||
if (mnemonic_source.kind != MNEMONIC_SOURCE_TUI) {
|
||||
fprintf(stderr, "nsigner: --mnemonic-stdin and --mnemonic-fd are mutually exclusive\n");
|
||||
@@ -3626,27 +3786,40 @@ int main(int argc, char *argv[]) {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
/* Non-interactive mode: create a default "main" role */
|
||||
role_entry_t role;
|
||||
memset(&role, 0, sizeof(role));
|
||||
strncpy(role.name, "main", sizeof(role.name) - 1);
|
||||
strncpy(role.purpose_str, "nostr", sizeof(role.purpose_str) - 1);
|
||||
strncpy(role.curve_str, "secp256k1", sizeof(role.curve_str) - 1);
|
||||
role.purpose = role_purpose_from_str(role.purpose_str);
|
||||
role.curve = role_curve_from_str(role.curve_str);
|
||||
role.selector_type = SELECTOR_ROLE_PATH;
|
||||
strncpy(role.role_path, "m/44'/1237'/0'/0/0", sizeof(role.role_path) - 1);
|
||||
role.role_path[sizeof(role.role_path) - 1] = '\0';
|
||||
role.nostr_index = -1;
|
||||
role.path_range_lo = -1;
|
||||
role.path_range_hi = -1;
|
||||
role.path_default_index = -1;
|
||||
role.requires_approval = 1;
|
||||
role.derived = 0;
|
||||
if (role_table_add(&role_table, &role) != 0) {
|
||||
fprintf(stderr, "Failed to initialize default role\n");
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
/* Non-interactive mode. If --register-role specs were provided,
|
||||
* register those roles (suppressing the default "main" role).
|
||||
* Otherwise, create a default "main" role with the standard
|
||||
* NIP-06 path. */
|
||||
if (register_role_count > 0) {
|
||||
for (int i = 0; i < register_role_count; ++i) {
|
||||
if (register_role_from_spec(&role_table, register_role_specs[i]) != 0) {
|
||||
fprintf(stderr, "Failed to register role from --register-role spec\n");
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
role_entry_t role;
|
||||
memset(&role, 0, sizeof(role));
|
||||
strncpy(role.name, "main", sizeof(role.name) - 1);
|
||||
strncpy(role.purpose_str, "nostr", sizeof(role.purpose_str) - 1);
|
||||
strncpy(role.curve_str, "secp256k1", sizeof(role.curve_str) - 1);
|
||||
role.purpose = role_purpose_from_str(role.purpose_str);
|
||||
role.curve = role_curve_from_str(role.curve_str);
|
||||
role.selector_type = SELECTOR_ROLE_PATH;
|
||||
strncpy(role.role_path, "m/44'/1237'/0'/0/0", sizeof(role.role_path) - 1);
|
||||
role.role_path[sizeof(role.role_path) - 1] = '\0';
|
||||
role.nostr_index = -1;
|
||||
role.path_range_lo = -1;
|
||||
role.path_range_hi = -1;
|
||||
role.path_default_index = -1;
|
||||
role.requires_approval = 1;
|
||||
role.derived = 0;
|
||||
if (role_table_add(&role_table, &role) != 0) {
|
||||
fprintf(stderr, "Failed to initialize default role\n");
|
||||
mnemonic_unload(&mnemonic);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3833,7 +4006,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
if (server_start(&server) != 0) {
|
||||
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||
fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server));
|
||||
fprintf(stderr, "Failed to start server on %s: %s\n", socket_name, server_last_error(&server));
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP ||
|
||||
listen_mode == NSIGNER_LISTEN_HTTP) {
|
||||
fprintf(stderr, "Failed to start server on %s: %s\n", listen_target, server_last_error(&server));
|
||||
@@ -3937,7 +4110,7 @@ int main(int argc, char *argv[]) {
|
||||
char unix_conn[128];
|
||||
char unix_example[256];
|
||||
char unix_extra[256] = "";
|
||||
snprintf(unix_conn, sizeof(unix_conn), "@%s", socket_name);
|
||||
snprintf(unix_conn, sizeof(unix_conn), "%s", socket_name);
|
||||
snprintf(unix_example, sizeof(unix_example),
|
||||
"nsigner --socket-name %s client '<json>'", socket_name);
|
||||
if (transport_mask & TRANSPORT_QREXEC_BRIDGE) {
|
||||
@@ -3948,7 +4121,7 @@ int main(int argc, char *argv[]) {
|
||||
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||
}
|
||||
connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
printf("System is ready and waiting for connections on %s.\n", socket_name);
|
||||
}
|
||||
if (transport_mask & TRANSPORT_TCP) {
|
||||
const char *tcp_addr = servers[tcp_server_idx].socket_name;
|
||||
@@ -3988,7 +4161,7 @@ int main(int argc, char *argv[]) {
|
||||
char unix_conn[128];
|
||||
char unix_example[256];
|
||||
char unix_extra[256] = "";
|
||||
snprintf(unix_conn, sizeof(unix_conn), "@%s", socket_name);
|
||||
snprintf(unix_conn, sizeof(unix_conn), "%s", socket_name);
|
||||
snprintf(unix_example, sizeof(unix_example),
|
||||
"nsigner --socket-name %s client '<json>'", socket_name);
|
||||
if (bridge_source_trusted) {
|
||||
@@ -3999,7 +4172,7 @@ int main(int argc, char *argv[]) {
|
||||
printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME);
|
||||
}
|
||||
connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra);
|
||||
printf("System is ready and waiting for connections on @%s.\n", socket_name);
|
||||
printf("System is ready and waiting for connections on %s.\n", socket_name);
|
||||
} else if (listen_mode == NSIGNER_LISTEN_TCP) {
|
||||
const char *actual_addr = server.socket_name;
|
||||
char fips_conn[256] = "";
|
||||
|
||||
+3
-3
@@ -2000,7 +2000,7 @@ int server_start(server_ctx_t *ctx) {
|
||||
}
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(@%s) failed: %s (and failed to generate retry socket name)",
|
||||
"bind(%s) failed: %s (and failed to generate retry socket name)",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
close(fd);
|
||||
@@ -2010,13 +2010,13 @@ int server_start(server_ctx_t *ctx) {
|
||||
if (errno == EADDRINUSE && ctx->socket_name_explicit) {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(@%s) failed: %s (explicit --socket-name is already in use)",
|
||||
"bind(%s) failed: %s (explicit --socket-name is already in use)",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
} else {
|
||||
(void)snprintf(ctx->last_error,
|
||||
sizeof(ctx->last_error),
|
||||
"bind(@%s) failed: %s",
|
||||
"bind(%s) failed: %s",
|
||||
ctx->socket_name,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
@@ -206,8 +206,55 @@ stop_server() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Second server with --register-role (template roles). Uses a separate socket.
|
||||
REGROLE_SOCKET_NAME=""
|
||||
REGROLE_SERVER_PID=""
|
||||
|
||||
start_server_regrole() {
|
||||
REGROLE_SOCKET_NAME="nsigner_test_regrole_$$"
|
||||
echo "Starting nsigner server with --register-role (socket: @$REGROLE_SOCKET_NAME)..."
|
||||
|
||||
export NSIGNER_TEST_NONINTERACTIVE_PROMPT=allow
|
||||
echo "$MNEMONIC" | "$SERVER" \
|
||||
--socket-name "$REGROLE_SOCKET_NAME" \
|
||||
--allow-all \
|
||||
--listen unix \
|
||||
--mnemonic-stdin \
|
||||
--register-role "nostr_range:secp256k1:m/44'/1237'/*'/0/0" \
|
||||
--register-role "ssh_range:ed25519:m/44'/102001'/*'/0'/0'" \
|
||||
--register-role "ml_dsa_range:ml-dsa-65:m/44'/102003'/*'/0'/0'" \
|
||||
--register-role "slh_dsa_range:slh-dsa-128s:m/44'/102004'/*'/0'/0'" \
|
||||
>/dev/null 2>&1 &
|
||||
REGROLE_SERVER_PID=$!
|
||||
|
||||
local max_attempts=50
|
||||
local attempt=0
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
if grep -q "$REGROLE_SOCKET_NAME" /proc/net/unix 2>/dev/null; then
|
||||
echo "Regrole server ready (PID $REGROLE_SERVER_PID, socket @$REGROLE_SOCKET_NAME)"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
echo "ERROR: regrole server did not become ready"
|
||||
kill "$REGROLE_SERVER_PID" 2>/dev/null
|
||||
REGROLE_SERVER_PID=""
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_server_regrole() {
|
||||
if [ -n "$REGROLE_SERVER_PID" ]; then
|
||||
echo "Stopping regrole server (PID $REGROLE_SERVER_PID)..."
|
||||
kill "$REGROLE_SERVER_PID" 2>/dev/null
|
||||
wait "$REGROLE_SERVER_PID" 2>/dev/null || true
|
||||
REGROLE_SERVER_PID=""
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
stop_server
|
||||
stop_server_regrole
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -589,6 +636,243 @@ test_ml_kem_roundtrip() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Template-role tests (require the regrole server with --register-role)
|
||||
# ---------------------------------------------------------------------------
|
||||
test_template_role_distinct_pubkeys() {
|
||||
echo ""
|
||||
echo "=== Template-role: distinct pubkeys per account index ==="
|
||||
|
||||
# This test catches the caching bug where a template role returned the
|
||||
# same pubkey for all concrete paths after the first derivation.
|
||||
local name="nostr_range index 0 returns 64 hex"
|
||||
local pk0
|
||||
pk0=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
if printf '%s\n' "$pk0" | grep -qE '^[0-9a-f]{64}$'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "got: $pk0"
|
||||
return
|
||||
fi
|
||||
|
||||
local pk1 pk5
|
||||
pk1=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/1'/0/0" get-public-key 2>/dev/null)
|
||||
pk5=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/5'/0/0" get-public-key 2>/dev/null)
|
||||
|
||||
if [ "$pk0" != "$pk1" ]; then
|
||||
pass "nostr_range index 0 != index 1 (distinct keys)"
|
||||
else
|
||||
fail "nostr_range index 0 != index 1 (distinct keys)" "both: $pk0"
|
||||
fi
|
||||
|
||||
if [ "$pk0" != "$pk5" ] && [ "$pk1" != "$pk5" ]; then
|
||||
pass "nostr_range index 5 distinct from 0 and 1"
|
||||
else
|
||||
fail "nostr_range index 5 distinct from 0 and 1" "pk0=$pk0 pk1=$pk1 pk5=$pk5"
|
||||
fi
|
||||
|
||||
# Re-request index 0 to confirm it's stable (not affected by caching)
|
||||
local pk0_again
|
||||
pk0_again=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null)
|
||||
if [ "$pk0" = "$pk0_again" ]; then
|
||||
pass "nostr_range index 0 stable on re-request"
|
||||
else
|
||||
fail "nostr_range index 0 stable on re-request" "first=$pk0 second=$pk0_again"
|
||||
fi
|
||||
}
|
||||
|
||||
test_template_role_path_rejection() {
|
||||
echo ""
|
||||
echo "=== Template-role: path validation rejection ==="
|
||||
|
||||
# Hardened tail should be rejected (role template has unhardened 0/0)
|
||||
local name="reject hardened tail (0'/0') with path_not_allowed"
|
||||
local output rc
|
||||
set +e
|
||||
output=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0'/0'" get-public-key 2>&1)
|
||||
rc=$?
|
||||
set -e
|
||||
if [ $rc -ne 0 ] && printf '%s\n' "$output" | grep -q 'path_not_allowed'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "rc=$rc output=$output"
|
||||
fi
|
||||
|
||||
# Wrong change segment should be rejected
|
||||
name="reject wrong change segment (1) with path_not_allowed"
|
||||
set +e
|
||||
output=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/1/0" get-public-key 2>&1)
|
||||
rc=$?
|
||||
set -e
|
||||
if [ $rc -ne 0 ] && printf '%s\n' "$output" | grep -q 'path_not_allowed'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "rc=$rc output=$output"
|
||||
fi
|
||||
|
||||
# Unknown role should be rejected
|
||||
name="reject unknown role with unknown_role"
|
||||
set +e
|
||||
output=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role bogus --path "m/44'/1237'/0'/0/0" get-public-key 2>&1)
|
||||
rc=$?
|
||||
set -e
|
||||
if [ $rc -ne 0 ] && printf '%s\n' "$output" | grep -q 'unknown_role'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "rc=$rc output=$output"
|
||||
fi
|
||||
}
|
||||
|
||||
test_template_role_sign_event() {
|
||||
echo ""
|
||||
echo "=== Template-role: sign-event with distinct indices ==="
|
||||
|
||||
local pk0 signed_pubkey
|
||||
pk0=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" get-public-key 2>/dev/null)
|
||||
|
||||
local name="sign-event via nostr_range index 0"
|
||||
local output
|
||||
output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/0'/0/0" sign-event 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
signed_pubkey=$(json_get "pubkey" "$output")
|
||||
if [ "$signed_pubkey" = "$pk0" ]; then
|
||||
pass "$name pubkey matches get-public-key"
|
||||
else
|
||||
fail "$name pubkey matches get-public-key" "expected $pk0, got $signed_pubkey"
|
||||
fi
|
||||
|
||||
# Sign with index 1 and confirm different pubkey
|
||||
local pk1 signed_pubkey1
|
||||
pk1=$($CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/1'/0/0" get-public-key 2>/dev/null)
|
||||
output=$(echo "$EVENT_JSON" | $CLIENT --socket-name "$REGROLE_SOCKET_NAME" --timeout 8000 --role nostr_range --path "m/44'/1237'/1'/0/0" sign-event 2>/dev/null) || {
|
||||
fail "sign-event via nostr_range index 1" "exit code $?"
|
||||
return
|
||||
}
|
||||
signed_pubkey1=$(json_get "pubkey" "$output")
|
||||
if [ "$signed_pubkey1" = "$pk1" ] && [ "$signed_pubkey1" != "$signed_pubkey" ]; then
|
||||
pass "sign-event nostr_range index 1 has correct and distinct pubkey"
|
||||
else
|
||||
fail "sign-event nostr_range index 1 has correct and distinct pubkey" "pk1=$pk1 signed=$signed_pubkey1 prev=$signed_pubkey"
|
||||
fi
|
||||
}
|
||||
|
||||
test_ml_dsa_65_roundtrip() {
|
||||
echo ""
|
||||
echo "=== ML-DSA-65 sign/verify round-trip ==="
|
||||
|
||||
local name="get-public-key --algorithm ml-dsa-65 --index 0"
|
||||
local output
|
||||
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 get-public-key 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
if printf '%s\n' "$output" | grep -q '"algorithm":"ml-dsa-65"' && \
|
||||
printf '%s\n' "$output" | grep -q '"public_key"'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "got: $output"
|
||||
return
|
||||
fi
|
||||
|
||||
name="sign --algorithm ml-dsa-65 --index 0 68656c6c6f"
|
||||
local sig_raw sig
|
||||
sig_raw=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 sign "68656c6c6f" 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
# Sign output is JSON: {"algorithm":"...","key_id":"...","signature":"<hex>"}
|
||||
sig=$(echo "$sig_raw" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('signature',''))" 2>/dev/null)
|
||||
if [ -z "$sig" ]; then
|
||||
# Fallback: maybe raw hex
|
||||
sig="$sig_raw"
|
||||
fi
|
||||
# ML-DSA-65 signatures are 3309 bytes = 6618 hex chars
|
||||
if printf '%s\n' "$sig" | grep -qE '^[0-9a-f]{6618}$'; then
|
||||
pass "$name (sig is 6618 hex chars)"
|
||||
else
|
||||
fail "$name (sig is 6618 hex chars)" "got length ${#sig}"
|
||||
return
|
||||
fi
|
||||
|
||||
name="verify --algorithm ml-dsa-65 --index 0 68656c6c6f (valid)"
|
||||
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 verify "68656c6c6f" "$sig" 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
if printf '%s\n' "$output" | grep -qi 'valid\|true\|verified'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "got: $output"
|
||||
fi
|
||||
|
||||
name="verify --algorithm ml-dsa-65 --index 0 68656c6c6f (invalid sig)"
|
||||
local wrong_sig
|
||||
wrong_sig=$(printf '%06618s' 0 | tr ' ' '0')
|
||||
set +e
|
||||
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm ml-dsa-65 --index 0 verify "68656c6c6f" "$wrong_sig" 2>/dev/null)
|
||||
local rc=$?
|
||||
set -e
|
||||
if [ $rc -ne 0 ] || printf '%s\n' "$output" | grep -qi 'invalid\|false\|not'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "rc=$rc output=$output"
|
||||
fi
|
||||
}
|
||||
|
||||
test_slh_dsa_128s_roundtrip() {
|
||||
echo ""
|
||||
echo "=== SLH-DSA-128s sign/verify round-trip ==="
|
||||
|
||||
local name="get-public-key --algorithm slh-dsa-128s --index 0"
|
||||
local output
|
||||
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm slh-dsa-128s --index 0 get-public-key 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
if printf '%s\n' "$output" | grep -q '"algorithm":"slh-dsa-128s"' && \
|
||||
printf '%s\n' "$output" | grep -q '"public_key"'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "got: $output"
|
||||
return
|
||||
fi
|
||||
|
||||
name="sign --algorithm slh-dsa-128s --index 0 68656c6c6f"
|
||||
local sig_raw sig
|
||||
sig_raw=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm slh-dsa-128s --index 0 sign "68656c6c6f" 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
# Sign output is JSON: {"algorithm":"...","key_id":"...","signature":"<hex>"}
|
||||
sig=$(echo "$sig_raw" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('signature',''))" 2>/dev/null)
|
||||
if [ -z "$sig" ]; then
|
||||
sig="$sig_raw"
|
||||
fi
|
||||
# SLH-DSA-128s signatures are 7856 bytes = 15712 hex chars
|
||||
if printf '%s\n' "$sig" | grep -qE '^[0-9a-f]{15712}$'; then
|
||||
pass "$name (sig is 15712 hex chars)"
|
||||
else
|
||||
fail "$name (sig is 15712 hex chars)" "got length ${#sig}"
|
||||
return
|
||||
fi
|
||||
|
||||
name="verify --algorithm slh-dsa-128s --index 0 68656c6c6f (valid)"
|
||||
output=$($CLIENT --socket-name "$SOCKET_NAME" --algorithm slh-dsa-128s --index 0 verify "68656c6c6f" "$sig" 2>/dev/null) || {
|
||||
fail "$name" "exit code $?"
|
||||
return
|
||||
}
|
||||
if printf '%s\n' "$output" | grep -qi 'valid\|true\|verified'; then
|
||||
pass "$name"
|
||||
else
|
||||
fail "$name" "got: $output"
|
||||
fi
|
||||
}
|
||||
|
||||
test_otp_encrypt_decrypt() {
|
||||
echo ""
|
||||
echo "=== OTP encrypt/decrypt ==="
|
||||
@@ -790,6 +1074,12 @@ start_server || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Start a second server with --register-role (template roles)
|
||||
start_server_regrole || {
|
||||
echo "FATAL: could not start nsigner regrole server"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run tests
|
||||
test_get_info
|
||||
test_get_public_key_nostr
|
||||
@@ -799,11 +1089,18 @@ test_nip04_roundtrip
|
||||
test_nip44_roundtrip
|
||||
test_algorithm_verbs
|
||||
test_ml_kem_roundtrip
|
||||
test_ml_dsa_65_roundtrip
|
||||
test_slh_dsa_128s_roundtrip
|
||||
test_otp_encrypt_decrypt
|
||||
test_call_verb
|
||||
test_error_cases
|
||||
test_auto_discovery
|
||||
|
||||
# Template-role tests (require regrole server)
|
||||
test_template_role_distinct_pubkeys
|
||||
test_template_role_path_rejection
|
||||
test_template_role_sign_event
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "============================================"
|
||||
|
||||
Reference in New Issue
Block a user