1118 lines
38 KiB
C
1118 lines
38 KiB
C
/*
|
|
* test_n_signer_client.c — C99 integration test for n_signer_client CLI surface.
|
|
*
|
|
* Spawns a dedicated nsigner server with a known test mnemonic on a unique
|
|
* abstract UNIX socket, then exercises the full verb surface through the
|
|
* nostr_core_lib client API (nsigner_client_call) and the high-level
|
|
* nostr_signer API. This mirrors what build/n_signer_client does internally.
|
|
*
|
|
* Build (from n_signer repo root):
|
|
* make test-n-signer-client
|
|
*
|
|
* The test is self-contained: it starts its own signer, runs tests, and
|
|
* tears down. It is deterministic (fixed mnemonic, fixed socket name, no
|
|
* reliance on external relays).
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "nostr_common.h"
|
|
#include "nsigner_transport.h"
|
|
#include "nsigner_client.h"
|
|
#include "nostr_signer.h"
|
|
#include "../cjson/cJSON.h"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Constants */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define SOCKET_NAME "nsigner_test_client_run"
|
|
#define MAX_MSG_SIZE 65536
|
|
#define TIMEOUT_MS 10000
|
|
|
|
static const char *MNEMONIC =
|
|
"abandon abandon abandon abandon abandon abandon abandon abandon "
|
|
"abandon abandon abandon about";
|
|
|
|
/* Test event JSON (kind 1, deterministic created_at) */
|
|
static const char *EVENT_JSON =
|
|
"{\"kind\":1,\"content\":\"hello from c99 test\",\"tags\":[],"
|
|
"\"created_at\":1700000000}";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test harness */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int g_passes = 0;
|
|
static int g_failures = 0;
|
|
static int g_skips = 0;
|
|
|
|
static void check_condition(const char *name, int condition) {
|
|
if (condition) {
|
|
printf(" PASS %s\n", name);
|
|
g_passes++;
|
|
} else {
|
|
printf(" FAIL %s\n", name);
|
|
g_failures++;
|
|
}
|
|
}
|
|
|
|
static void skip_test(const char *name, const char *reason) {
|
|
(void)reason;
|
|
printf(" SKIP %s (%s)\n", name, reason ? reason : "");
|
|
g_skips++;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int sleep_ms(int ms) {
|
|
struct timespec ts;
|
|
ts.tv_sec = ms / 1000;
|
|
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
|
|
return nanosleep(&ts, NULL);
|
|
}
|
|
|
|
static int write_full(int fd, const void *buf, size_t len) {
|
|
const unsigned char *p = (const unsigned char *)buf;
|
|
size_t off = 0;
|
|
while (off < len) {
|
|
ssize_t n = write(fd, p + off, len - off);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
return -1;
|
|
}
|
|
off += (size_t)n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Check if a string contains a substring */
|
|
static int str_contains(const char *haystack, const char *needle) {
|
|
if (!haystack || !needle) return 0;
|
|
return strstr(haystack, needle) != NULL;
|
|
}
|
|
|
|
/* Check if a string is all hex chars of exact length */
|
|
static int is_hex_len(const char *s, size_t expected_len) {
|
|
size_t i;
|
|
if (!s) return 0;
|
|
if (strlen(s) != expected_len) return 0;
|
|
for (i = 0; i < expected_len; i++) {
|
|
if (!isxdigit((unsigned char)s[i])) return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Low-level RPC helper (fresh connection per call, like n_signer) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* Make a single RPC call to the signer. Returns a cJSON result object
|
|
* (caller must delete) or NULL on failure. `params` ownership is always
|
|
* transferred to the client (consumed on success or freed on failure). */
|
|
static cJSON *rpc_call(const char *method, cJSON *params) {
|
|
nsigner_transport_t *t = NULL;
|
|
nsigner_client_t *c = NULL;
|
|
cJSON *result = NULL;
|
|
|
|
t = nsigner_transport_open_unix(SOCKET_NAME, TIMEOUT_MS);
|
|
if (!t) {
|
|
cJSON_Delete(params);
|
|
return NULL;
|
|
}
|
|
c = nsigner_client_new(t);
|
|
if (!c) {
|
|
t->close(t);
|
|
cJSON_Delete(params);
|
|
return NULL;
|
|
}
|
|
if (nsigner_client_call(c, method, params, &result) != NOSTR_SUCCESS) {
|
|
result = NULL;
|
|
}
|
|
/* nsigner_client_free closes/frees the transport too */
|
|
nsigner_client_free(c);
|
|
return result;
|
|
}
|
|
|
|
/* Extract a string field from a result that may be either a JSON string
|
|
* (the server returns result as a JSON-encoded string) or a JSON object.
|
|
* Returns a malloc'd string or NULL. */
|
|
static char *result_string_field(cJSON *result, const char *field) {
|
|
cJSON *parsed = NULL;
|
|
cJSON *item = NULL;
|
|
char *out = NULL;
|
|
|
|
if (!result) return NULL;
|
|
|
|
if (cJSON_IsString(result)) {
|
|
/* result is a JSON-encoded string; parse it */
|
|
parsed = cJSON_Parse(result->valuestring);
|
|
if (!parsed) return NULL;
|
|
item = cJSON_GetObjectItemCaseSensitive(parsed, field);
|
|
if (item && cJSON_IsString(item)) {
|
|
out = strdup(item->valuestring);
|
|
}
|
|
cJSON_Delete(parsed);
|
|
} else if (cJSON_IsObject(result)) {
|
|
item = cJSON_GetObjectItemCaseSensitive(result, field);
|
|
if (item && cJSON_IsString(item)) {
|
|
out = strdup(item->valuestring);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* Check if a result (string or object) contains a field */
|
|
static int result_has_field(cJSON *result, const char *field) {
|
|
cJSON *parsed = NULL;
|
|
int found = 0;
|
|
|
|
if (!result) return 0;
|
|
|
|
if (cJSON_IsString(result)) {
|
|
parsed = cJSON_Parse(result->valuestring);
|
|
if (parsed) {
|
|
found = (cJSON_GetObjectItemCaseSensitive(parsed, field) != NULL);
|
|
cJSON_Delete(parsed);
|
|
}
|
|
} else if (cJSON_IsObject(result)) {
|
|
found = (cJSON_GetObjectItemCaseSensitive(result, field) != NULL);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/* Get the raw result as a string (for debugging / grep-like checks).
|
|
* Returns malloc'd string or NULL. */
|
|
static char *result_to_string(cJSON *result) {
|
|
if (!result) return NULL;
|
|
if (cJSON_IsString(result)) {
|
|
return strdup(result->valuestring);
|
|
}
|
|
return cJSON_PrintUnformatted(result);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Server management */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static pid_t g_server_pid = -1;
|
|
|
|
static int start_server(void) {
|
|
int stdin_pipe[2];
|
|
pid_t child;
|
|
int null_fd;
|
|
|
|
if (pipe(stdin_pipe) != 0) {
|
|
perror("pipe");
|
|
return -1;
|
|
}
|
|
|
|
child = fork();
|
|
if (child < 0) {
|
|
perror("fork");
|
|
close(stdin_pipe[0]);
|
|
close(stdin_pipe[1]);
|
|
return -1;
|
|
}
|
|
|
|
if (child == 0) {
|
|
/* Child: redirect stdout/stderr to /dev/null, stdin from pipe */
|
|
null_fd = open("/dev/null", O_WRONLY);
|
|
if (null_fd >= 0) {
|
|
dup2(null_fd, STDOUT_FILENO);
|
|
dup2(null_fd, STDERR_FILENO);
|
|
close(null_fd);
|
|
}
|
|
|
|
/* Force non-interactive prompt auto-allow for tests */
|
|
(void)setenv("NSIGNER_TEST_FORCE_PROMPT", "1", 1);
|
|
(void)setenv("NSIGNER_TEST_NONINTERACTIVE_PROMPT", "allow", 1);
|
|
|
|
dup2(stdin_pipe[0], STDIN_FILENO);
|
|
close(stdin_pipe[0]);
|
|
close(stdin_pipe[1]);
|
|
|
|
execl("./build/nsigner", "./build/nsigner",
|
|
"--socket-name", SOCKET_NAME,
|
|
"--allow-all",
|
|
"--listen", "unix",
|
|
"--mnemonic-stdin",
|
|
(char *)NULL);
|
|
_exit(127);
|
|
}
|
|
|
|
/* Parent: feed mnemonic to child's stdin */
|
|
close(stdin_pipe[0]);
|
|
{
|
|
/* Write mnemonic with a leading newline (the server reads one line) */
|
|
char buf[300];
|
|
snprintf(buf, sizeof(buf), "\n%s\n", MNEMONIC);
|
|
(void)write_full(stdin_pipe[1], buf, strlen(buf));
|
|
}
|
|
close(stdin_pipe[1]);
|
|
|
|
g_server_pid = child;
|
|
|
|
/* Wait for the server to be ready by polling the socket */
|
|
{
|
|
int elapsed = 0;
|
|
while (elapsed < 5000) {
|
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
struct sockaddr_un addr;
|
|
socklen_t addr_len;
|
|
if (fd < 0) {
|
|
sleep_ms(100);
|
|
elapsed += 100;
|
|
continue;
|
|
}
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sun_family = AF_UNIX;
|
|
addr.sun_path[0] = '\0';
|
|
strncpy(&addr.sun_path[1], SOCKET_NAME, sizeof(addr.sun_path) - 2);
|
|
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
|
|
addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(SOCKET_NAME));
|
|
if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
|
|
close(fd);
|
|
printf("Server ready (PID %d, socket @%s)\n",
|
|
(int)g_server_pid, SOCKET_NAME);
|
|
return 0;
|
|
}
|
|
close(fd);
|
|
sleep_ms(100);
|
|
elapsed += 100;
|
|
}
|
|
}
|
|
|
|
fprintf(stderr, "ERROR: server did not become ready\n");
|
|
return -1;
|
|
}
|
|
|
|
static void stop_server(void) {
|
|
int status;
|
|
if (g_server_pid > 0) {
|
|
(void)kill(g_server_pid, SIGTERM);
|
|
(void)waitpid(g_server_pid, &status, 0);
|
|
g_server_pid = -1;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test cases */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* --- Basic connectivity --- */
|
|
|
|
static void test_get_info(void) {
|
|
cJSON *result;
|
|
char *raw;
|
|
|
|
printf("\n=== Basic connectivity ===\n");
|
|
|
|
result = rpc_call("get_info", cJSON_CreateArray());
|
|
raw = result_to_string(result);
|
|
check_condition("get-info returns server metadata",
|
|
result != NULL &&
|
|
str_contains(raw, "name") &&
|
|
str_contains(raw, "verbs") &&
|
|
str_contains(raw, "algorithms"));
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
static void test_get_public_key_nostr(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *pubkey;
|
|
|
|
/* get-public-key (nostr, default role) → 64 hex chars */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
pubkey = result_to_string(result);
|
|
check_condition("get-public-key (nostr, default role) returns 64 hex chars",
|
|
pubkey != NULL && is_hex_len(pubkey, 64));
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
|
|
/* get-public-key --nostr-index 0 → 64 hex chars (same as above) */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
pubkey = result_to_string(result);
|
|
check_condition("get-public-key --nostr-index 0 returns 64 hex chars",
|
|
pubkey != NULL && is_hex_len(pubkey, 64));
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
|
|
/* get-public-key --nostr-index 0 --format structured → JSON with
|
|
* algorithm and public_key */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddStringToObject(opts, "format", "structured");
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
raw = result_to_string(result);
|
|
check_condition("get-public-key --nostr-index 0 --format structured returns JSON",
|
|
raw != NULL &&
|
|
str_contains(raw, "algorithm") &&
|
|
str_contains(raw, "public_key"));
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- Sign event --- */
|
|
|
|
static void test_sign_event(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *raw, *pubkey, *signed_pubkey, *sig;
|
|
char expected_pubkey[65];
|
|
|
|
printf("\n=== Sign event ===\n");
|
|
|
|
/* Get the expected pubkey first */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
pubkey = result_to_string(result);
|
|
if (pubkey && is_hex_len(pubkey, 64)) {
|
|
strncpy(expected_pubkey, pubkey, 64);
|
|
expected_pubkey[64] = '\0';
|
|
} else {
|
|
expected_pubkey[0] = '\0';
|
|
}
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
|
|
/* sign-event from argv (we pass event JSON as a string param) */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(EVENT_JSON));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_sign_event", params);
|
|
raw = result_to_string(result);
|
|
check_condition("sign-event returns signed event JSON",
|
|
raw != NULL &&
|
|
str_contains(raw, "id") &&
|
|
str_contains(raw, "pubkey") &&
|
|
str_contains(raw, "sig"));
|
|
free(raw);
|
|
|
|
/* Verify the signed event's pubkey matches get-public-key output */
|
|
signed_pubkey = result_string_field(result, "pubkey");
|
|
check_condition("sign-event pubkey matches get-public-key",
|
|
signed_pubkey != NULL &&
|
|
expected_pubkey[0] != '\0' &&
|
|
strcmp(signed_pubkey, expected_pubkey) == 0);
|
|
|
|
/* Verify the signature is 128 hex chars (schnorr) */
|
|
sig = result_string_field(result, "sig");
|
|
check_condition("sign-event sig is 128 hex chars",
|
|
sig != NULL && is_hex_len(sig, 128));
|
|
|
|
free(signed_pubkey);
|
|
free(sig);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- Mine event --- */
|
|
|
|
static void test_mine_event(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *raw;
|
|
|
|
printf("\n=== Mine event ===\n");
|
|
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(EVENT_JSON));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddNumberToObject(opts, "difficulty", 4);
|
|
cJSON_AddNumberToObject(opts, "timeout_sec", 10);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_mine_event", params);
|
|
raw = result_to_string(result);
|
|
/* mine-event wraps the signed event in an "event" field and adds
|
|
* achieved_difficulty / target_reached */
|
|
check_condition("mine-event with difficulty 4 returns mined event JSON",
|
|
raw != NULL &&
|
|
str_contains(raw, "event") &&
|
|
str_contains(raw, "achieved_difficulty") &&
|
|
str_contains(raw, "target_reached"));
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- NIP-04 / NIP-44 encrypt/decrypt round-trips --- */
|
|
|
|
static void test_nip04_roundtrip(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *pubkey, *ciphertext, *decrypted;
|
|
const char *plaintext = "hello_nip04_test";
|
|
|
|
printf("\n=== NIP-04 encrypt/decrypt round-trip ===\n");
|
|
|
|
/* Get our own pubkey to use as peer (encrypt to self) */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
pubkey = result_to_string(result);
|
|
cJSON_Delete(result);
|
|
|
|
if (!pubkey || !is_hex_len(pubkey, 64)) {
|
|
check_condition("nip04-encrypt returns ciphertext", 0);
|
|
free(pubkey);
|
|
return;
|
|
}
|
|
|
|
/* nip04-encrypt */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(pubkey));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_nip04_encrypt", params);
|
|
ciphertext = result_to_string(result);
|
|
check_condition("nip04-encrypt returns ciphertext",
|
|
ciphertext != NULL && strlen(ciphertext) > 0);
|
|
|
|
if (!ciphertext) {
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
return;
|
|
}
|
|
|
|
/* nip04-decrypt */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(pubkey));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
{
|
|
cJSON *dec_result = rpc_call("nostr_nip04_decrypt", params);
|
|
decrypted = result_to_string(dec_result);
|
|
check_condition("nip04-decrypt recovers plaintext",
|
|
decrypted != NULL && strcmp(decrypted, plaintext) == 0);
|
|
free(decrypted);
|
|
cJSON_Delete(dec_result);
|
|
}
|
|
|
|
free(ciphertext);
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
static void test_nip44_roundtrip(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *pubkey, *ciphertext, *decrypted;
|
|
const char *plaintext = "hello_nip44_test";
|
|
|
|
printf("\n=== NIP-44 encrypt/decrypt round-trip ===\n");
|
|
|
|
/* Get our own pubkey to use as peer */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
pubkey = result_to_string(result);
|
|
cJSON_Delete(result);
|
|
|
|
if (!pubkey || !is_hex_len(pubkey, 64)) {
|
|
check_condition("nip44-encrypt returns ciphertext", 0);
|
|
free(pubkey);
|
|
return;
|
|
}
|
|
|
|
/* nip44-encrypt */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(pubkey));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_nip44_encrypt", params);
|
|
ciphertext = result_to_string(result);
|
|
check_condition("nip44-encrypt returns ciphertext",
|
|
ciphertext != NULL && strlen(ciphertext) > 0);
|
|
|
|
if (!ciphertext) {
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
return;
|
|
}
|
|
|
|
/* nip44-decrypt */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(pubkey));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
{
|
|
cJSON *dec_result = rpc_call("nostr_nip44_decrypt", params);
|
|
decrypted = result_to_string(dec_result);
|
|
check_condition("nip44-decrypt recovers plaintext",
|
|
decrypted != NULL && strcmp(decrypted, plaintext) == 0);
|
|
free(decrypted);
|
|
cJSON_Delete(dec_result);
|
|
}
|
|
|
|
free(ciphertext);
|
|
free(pubkey);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- Algorithm-based verbs --- */
|
|
|
|
static void test_algorithm_verbs(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *raw, *sig, *digest, *shared_secret;
|
|
|
|
printf("\n=== Algorithm-based verbs ===\n");
|
|
|
|
/* get-public-key --algorithm secp256k1 --index 0 */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "secp256k1");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("get_public_key", params);
|
|
raw = result_to_string(result);
|
|
check_condition("get-public-key --algorithm secp256k1 --index 0",
|
|
raw != NULL &&
|
|
str_contains(raw, "secp256k1") &&
|
|
str_contains(raw, "public_key"));
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
|
|
/* get-public-key --algorithm ed25519 --index 0 */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("get_public_key", params);
|
|
raw = result_to_string(result);
|
|
check_condition("get-public-key --algorithm ed25519 --index 0",
|
|
raw != NULL && str_contains(raw, "ed25519"));
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
|
|
/* sign --algorithm ed25519 --index 0 68656c6c6f (hex for "hello") */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString("68656c6c6f"));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("sign", params);
|
|
raw = result_to_string(result);
|
|
check_condition("sign --algorithm ed25519 --index 0 68656c6c6f",
|
|
raw != NULL && str_contains(raw, "signature"));
|
|
sig = result_string_field(result, "signature");
|
|
free(raw);
|
|
|
|
if (sig) {
|
|
/* verify --algorithm ed25519 --index 0 68656c6c6f <sig> (valid) */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString("68656c6c6f"));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(sig));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
{
|
|
cJSON *vresult = rpc_call("verify", params);
|
|
raw = result_to_string(vresult);
|
|
check_condition("verify --algorithm ed25519 (valid sig)",
|
|
raw != NULL && str_contains(raw, "valid"));
|
|
free(raw);
|
|
cJSON_Delete(vresult);
|
|
}
|
|
|
|
/* verify --algorithm ed25519 --index 0 68656c6c6f <wrong-sig> (invalid) */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString("68656c6c6f"));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(
|
|
"abcdef0123456789abcdef0123456789abcdef0123456789"
|
|
"abcdef0123456789abcdef0123456789abcdef0123456789"
|
|
"abcdef0123456789abcdef0123456789abcdef01"));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
{
|
|
cJSON *vresult = rpc_call("verify", params);
|
|
raw = result_to_string(vresult);
|
|
check_condition("verify --algorithm ed25519 (invalid sig)",
|
|
raw != NULL && str_contains(raw, "invalid"));
|
|
free(raw);
|
|
cJSON_Delete(vresult);
|
|
}
|
|
free(sig);
|
|
} else {
|
|
check_condition("verify --algorithm ed25519 (valid sig)", 0);
|
|
check_condition("verify --algorithm ed25519 (invalid sig)", 0);
|
|
}
|
|
|
|
/* derive --algorithm secp256k1 --index 0 'test-data' */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString("test-data"));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "secp256k1");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("derive", params);
|
|
raw = result_to_string(result);
|
|
digest = result_string_field(result, "digest");
|
|
check_condition("derive --algorithm secp256k1 --index 0 'test-data'",
|
|
digest != NULL && is_hex_len(digest, 64));
|
|
free(digest);
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
|
|
/* derive-shared-secret --algorithm x25519 --index 0 <peer-pubkey> */
|
|
{
|
|
char *peer_pubkey;
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
peer_pubkey = result_to_string(result);
|
|
cJSON_Delete(result);
|
|
|
|
if (peer_pubkey && is_hex_len(peer_pubkey, 64)) {
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "x25519");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("derive_shared_secret", params);
|
|
/* The server returns structured JSON with shared_secret field */
|
|
shared_secret = result_string_field(result, "shared_secret");
|
|
check_condition("derive-shared-secret --algorithm x25519 --index 0",
|
|
shared_secret != NULL && is_hex_len(shared_secret, 64));
|
|
free(shared_secret);
|
|
cJSON_Delete(result);
|
|
} else {
|
|
check_condition("derive-shared-secret --algorithm x25519 --index 0", 0);
|
|
}
|
|
free(peer_pubkey);
|
|
}
|
|
}
|
|
|
|
/* --- ML-KEM-768 encapsulate/decapsulate round-trip --- */
|
|
|
|
static void test_ml_kem_roundtrip(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *raw, *mlkem_pubkey, *ciphertext, *enc_ss, *dec_ss;
|
|
|
|
printf("\n=== ML-KEM-768 encapsulate/decapsulate round-trip ===\n");
|
|
|
|
/* get-public-key --algorithm ml-kem-768 --index 0 */
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("get_public_key", params);
|
|
raw = result_to_string(result);
|
|
check_condition("get-public-key --algorithm ml-kem-768 --index 0",
|
|
raw != NULL &&
|
|
str_contains(raw, "ml-kem-768") &&
|
|
str_contains(raw, "public_key"));
|
|
mlkem_pubkey = result_string_field(result, "public_key");
|
|
free(raw);
|
|
|
|
if (!mlkem_pubkey) {
|
|
check_condition("encapsulate --algorithm ml-kem-768", 0);
|
|
check_condition("decapsulate --algorithm ml-kem-768 --index 0", 0);
|
|
check_condition("ML-KEM-768 shared secrets match", 0);
|
|
cJSON_Delete(result);
|
|
return;
|
|
}
|
|
|
|
/* encapsulate --algorithm ml-kem-768 <peer-pubkey> */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(mlkem_pubkey));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("encapsulate", params);
|
|
raw = result_to_string(result);
|
|
check_condition("encapsulate --algorithm ml-kem-768 with peer pubkey",
|
|
raw != NULL &&
|
|
str_contains(raw, "ciphertext") &&
|
|
str_contains(raw, "shared_secret"));
|
|
ciphertext = result_string_field(result, "ciphertext");
|
|
enc_ss = result_string_field(result, "shared_secret");
|
|
free(raw);
|
|
|
|
if (!ciphertext || !enc_ss) {
|
|
check_condition("decapsulate --algorithm ml-kem-768 --index 0", 0);
|
|
check_condition("ML-KEM-768 shared secrets match", 0);
|
|
free(ciphertext);
|
|
free(enc_ss);
|
|
free(mlkem_pubkey);
|
|
cJSON_Delete(result);
|
|
return;
|
|
}
|
|
|
|
/* decapsulate --algorithm ml-kem-768 --index 0 <ciphertext> */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
{
|
|
cJSON *dec_result = rpc_call("decapsulate", params);
|
|
raw = result_to_string(dec_result);
|
|
check_condition("decapsulate --algorithm ml-kem-768 --index 0 with ciphertext",
|
|
raw != NULL && str_contains(raw, "shared_secret"));
|
|
dec_ss = result_string_field(dec_result, "shared_secret");
|
|
free(raw);
|
|
|
|
/* Verify shared secrets match */
|
|
check_condition("ML-KEM-768 encapsulate/decapsulate shared secrets match",
|
|
dec_ss != NULL && strcmp(enc_ss, dec_ss) == 0);
|
|
|
|
free(dec_ss);
|
|
cJSON_Delete(dec_result);
|
|
}
|
|
|
|
free(ciphertext);
|
|
free(enc_ss);
|
|
free(mlkem_pubkey);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- OTP encrypt/decrypt --- */
|
|
|
|
static void test_otp_encrypt_decrypt(void) {
|
|
cJSON *params, *opts, *result;
|
|
char *raw, *ciphertext, *decrypted;
|
|
const char *plaintext_b64 = "SGVsbG8gT1RQIQ=="; /* "Hello OTP!" in base64 */
|
|
|
|
printf("\n=== OTP encrypt/decrypt ===\n");
|
|
|
|
/* encrypt --algorithm otp <base64-plaintext> */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext_b64));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "otp");
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("encrypt", params);
|
|
|
|
if (!result) {
|
|
/* OTP requires a pad to be bound. If the signer wasn't started
|
|
* with --otp-pad, skip gracefully. */
|
|
skip_test("encrypt --algorithm otp", "OTP pad not available on server");
|
|
skip_test("decrypt --algorithm otp", "OTP pad not available on server");
|
|
return;
|
|
}
|
|
|
|
raw = result_to_string(result);
|
|
ciphertext = strdup(raw ? raw : "");
|
|
check_condition("encrypt --algorithm otp (base64 plaintext)",
|
|
ciphertext != NULL && strlen(ciphertext) > 0);
|
|
free(raw);
|
|
|
|
if (!ciphertext || strlen(ciphertext) == 0) {
|
|
free(ciphertext);
|
|
cJSON_Delete(result);
|
|
return;
|
|
}
|
|
|
|
/* decrypt --algorithm otp <ciphertext> */
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "otp");
|
|
cJSON_AddItemToArray(params, opts);
|
|
{
|
|
cJSON *dec_result = rpc_call("decrypt", params);
|
|
raw = result_to_string(dec_result);
|
|
check_condition("decrypt --algorithm otp",
|
|
raw != NULL && strlen(raw) > 0);
|
|
free(raw);
|
|
cJSON_Delete(dec_result);
|
|
}
|
|
|
|
free(ciphertext);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- Generic call verb --- */
|
|
|
|
static void test_call_verb(void) {
|
|
cJSON *result;
|
|
char *raw;
|
|
|
|
printf("\n=== Generic call verb ===\n");
|
|
|
|
/* echo '[]' | n_signer_client call get_info */
|
|
result = rpc_call("get_info", cJSON_CreateArray());
|
|
raw = result_to_string(result);
|
|
check_condition("call get_info via empty params",
|
|
raw != NULL &&
|
|
str_contains(raw, "name") &&
|
|
str_contains(raw, "verbs"));
|
|
free(raw);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* --- Error cases --- */
|
|
|
|
static void test_error_cases(void) {
|
|
printf("\n=== Error cases ===\n");
|
|
|
|
/* No socket found: try connecting to a bogus socket name.
|
|
* We can't easily test this via rpc_call (it uses a fixed socket),
|
|
* so we test at the transport level. */
|
|
{
|
|
nsigner_transport_t *t;
|
|
t = nsigner_transport_open_unix("nonexistent_socket_test", 1000);
|
|
check_condition("No socket found (bogus socket name)",
|
|
t == NULL);
|
|
if (t) t->close(t);
|
|
}
|
|
|
|
/* Conflicting selectors: send both nostr_index and role.
|
|
* The server should reject with ambiguous_role_selector (1001). */
|
|
{
|
|
cJSON *params, *opts, *result;
|
|
params = cJSON_CreateArray();
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(opts, "nostr_index", 0);
|
|
cJSON_AddStringToObject(opts, "role", "main");
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("nostr_get_public_key", params);
|
|
check_condition("Conflicting selectors (nostr_index + role) rejected",
|
|
result == NULL);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* Unknown verb: the server should return method not found (-32601) */
|
|
{
|
|
cJSON *result;
|
|
result = rpc_call("nonexistent_verb", cJSON_CreateArray());
|
|
check_condition("Unknown verb rejected",
|
|
result == NULL);
|
|
cJSON_Delete(result);
|
|
}
|
|
|
|
/* verify with malformed signature: should return an error, not
|
|
* "invalid". The server returns an error code. */
|
|
{
|
|
cJSON *params, *opts, *result;
|
|
params = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(params, cJSON_CreateString("68656c6c6f"));
|
|
cJSON_AddItemToArray(params, cJSON_CreateString("nothex"));
|
|
opts = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(opts, "algorithm", "ed25519");
|
|
cJSON_AddNumberToObject(opts, "index", 0);
|
|
cJSON_AddItemToArray(params, opts);
|
|
result = rpc_call("verify", params);
|
|
check_condition("verify with malformed signature returns error",
|
|
result == NULL);
|
|
cJSON_Delete(result);
|
|
}
|
|
}
|
|
|
|
/* --- High-level nostr_signer API (mirrors demo_c99.c) --- */
|
|
|
|
static void test_highlevel_api(void) {
|
|
nostr_signer_t *signer = NULL;
|
|
char pubkey_hex[65];
|
|
int rc;
|
|
|
|
printf("\n=== High-level nostr_signer API ===\n");
|
|
|
|
/* Create a high-level signer backed by UNIX socket transport */
|
|
signer = nostr_signer_nsigner_unix(SOCKET_NAME, NULL, TIMEOUT_MS);
|
|
check_condition("nostr_signer_nsigner_unix creates signer",
|
|
signer != NULL);
|
|
if (!signer) return;
|
|
|
|
/* Select key by nostr_index 0 */
|
|
rc = nostr_signer_nsigner_set_nostr_index(signer, 0);
|
|
check_condition("nostr_signer_nsigner_set_nostr_index(0)",
|
|
rc == NOSTR_SUCCESS);
|
|
|
|
/* get_public_key via high-level API */
|
|
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
|
check_condition("nostr_signer_get_public_key returns 64 hex chars",
|
|
rc == NOSTR_SUCCESS && is_hex_len(pubkey_hex, 64));
|
|
|
|
/* sign_event via high-level API */
|
|
if (rc == NOSTR_SUCCESS) {
|
|
cJSON *unsigned_event = cJSON_CreateObject();
|
|
cJSON *signed_event = NULL;
|
|
char *signed_json;
|
|
|
|
cJSON_AddNumberToObject(unsigned_event, "kind", 1);
|
|
cJSON_AddStringToObject(unsigned_event, "content",
|
|
"Hello from high-level API test!");
|
|
cJSON_AddNumberToObject(unsigned_event, "created_at", 1700000000);
|
|
cJSON_AddItemToObject(unsigned_event, "tags", cJSON_CreateArray());
|
|
cJSON_AddStringToObject(unsigned_event, "pubkey", pubkey_hex);
|
|
|
|
rc = nostr_signer_sign_event(signer, unsigned_event, &signed_event);
|
|
signed_json = signed_event ? cJSON_PrintUnformatted(signed_event) : NULL;
|
|
check_condition("nostr_signer_sign_event returns signed event",
|
|
rc == NOSTR_SUCCESS &&
|
|
signed_json != NULL &&
|
|
str_contains(signed_json, "id") &&
|
|
str_contains(signed_json, "sig"));
|
|
free(signed_json);
|
|
cJSON_Delete(signed_event);
|
|
cJSON_Delete(unsigned_event);
|
|
}
|
|
|
|
/* NIP-44 encrypt/decrypt via high-level API */
|
|
if (rc == NOSTR_SUCCESS) {
|
|
const char *plaintext = "hello_hl_nip44";
|
|
char *ciphertext = NULL;
|
|
char *decrypted = NULL;
|
|
|
|
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext,
|
|
&ciphertext);
|
|
check_condition("nostr_signer_nip44_encrypt returns ciphertext",
|
|
rc == NOSTR_SUCCESS && ciphertext != NULL);
|
|
|
|
if (rc == NOSTR_SUCCESS && ciphertext) {
|
|
rc = nostr_signer_nip44_decrypt(signer, pubkey_hex, ciphertext,
|
|
&decrypted);
|
|
check_condition("nostr_signer_nip44_decrypt recovers plaintext",
|
|
rc == NOSTR_SUCCESS &&
|
|
decrypted != NULL &&
|
|
strcmp(decrypted, plaintext) == 0);
|
|
free(decrypted);
|
|
}
|
|
free(ciphertext);
|
|
}
|
|
|
|
/* NIP-04 encrypt/decrypt via high-level API */
|
|
{
|
|
const char *plaintext = "hello_hl_nip04";
|
|
char *ciphertext = NULL;
|
|
char *decrypted = NULL;
|
|
|
|
rc = nostr_signer_nip04_encrypt(signer, pubkey_hex, plaintext,
|
|
&ciphertext);
|
|
check_condition("nostr_signer_nip04_encrypt returns ciphertext",
|
|
rc == NOSTR_SUCCESS && ciphertext != NULL);
|
|
|
|
if (rc == NOSTR_SUCCESS && ciphertext) {
|
|
rc = nostr_signer_nip04_decrypt(signer, pubkey_hex, ciphertext,
|
|
&decrypted);
|
|
check_condition("nostr_signer_nip04_decrypt recovers plaintext",
|
|
rc == NOSTR_SUCCESS &&
|
|
decrypted != NULL &&
|
|
strcmp(decrypted, plaintext) == 0);
|
|
free(decrypted);
|
|
}
|
|
free(ciphertext);
|
|
}
|
|
|
|
nostr_signer_free(signer);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Main */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int main(void) {
|
|
int total;
|
|
|
|
(void)signal(SIGPIPE, SIG_IGN);
|
|
|
|
printf("============================================\n");
|
|
printf(" n_signer_client C99 Integration Test\n");
|
|
printf("============================================\n");
|
|
printf("\n");
|
|
|
|
/* Initialize crypto subsystem */
|
|
if (nostr_init() != NOSTR_SUCCESS) {
|
|
fprintf(stderr, "FATAL: nostr_init() failed\n");
|
|
return 1;
|
|
}
|
|
|
|
/* Start the server */
|
|
if (start_server() != 0) {
|
|
fprintf(stderr, "FATAL: could not start nsigner server\n");
|
|
nostr_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
/* Run tests */
|
|
test_get_info();
|
|
test_get_public_key_nostr();
|
|
test_sign_event();
|
|
test_mine_event();
|
|
test_nip04_roundtrip();
|
|
test_nip44_roundtrip();
|
|
test_algorithm_verbs();
|
|
test_ml_kem_roundtrip();
|
|
test_otp_encrypt_decrypt();
|
|
test_call_verb();
|
|
test_error_cases();
|
|
test_highlevel_api();
|
|
|
|
/* Tear down */
|
|
stop_server();
|
|
nostr_cleanup();
|
|
|
|
/* Summary */
|
|
total = g_passes + g_failures + g_skips;
|
|
printf("\n");
|
|
printf("============================================\n");
|
|
printf(" Results\n");
|
|
printf("============================================\n");
|
|
printf(" PASS: %d\n", g_passes);
|
|
printf(" FAIL: %d\n", g_failures);
|
|
printf(" SKIP: %d\n", g_skips);
|
|
printf(" TOTAL: %d\n", total);
|
|
printf("============================================\n");
|
|
|
|
if (g_failures > 0) {
|
|
printf("TESTS FAILED: %d\n", g_failures);
|
|
return 1;
|
|
}
|
|
printf("ALL TESTS PASSED\n");
|
|
return 0;
|
|
}
|