Files
n_signer/tests/test_n_signer_client.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;
}