This commit is contained in:
Laan Tungir
2026-05-02 12:31:26 -04:00
commit 268b33b6d3
37 changed files with 7947 additions and 0 deletions

158
tests/test_dispatcher.c Normal file
View File

@@ -0,0 +1,158 @@
#include "../src/dispatcher.h"
#include "../src/enforcement.h"
#include "../src/role_table.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static int response_has(const char *response, const char *needle) {
return (response != NULL && needle != NULL && strstr(response, needle) != NULL);
}
static role_entry_t make_nostr_entry(const char *name, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_path_entry(const char *name, const char *purpose, const char *curve, const char *path) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, purpose, sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, curve, sizeof(e.curve_str) - 1);
strncpy(e.role_path, path, sizeof(e.role_path) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_ROLE_PATH;
e.nostr_index = -1;
e.derived = 0;
return e;
}
int main(void) {
role_table_t table;
role_entry_t main_role;
role_entry_t ssh_role;
mnemonic_state_t mnemonic;
dispatcher_ctx_t dispatcher;
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
char *resp;
role_table_init(&table);
main_role = make_nostr_entry("main", 0);
ssh_role = make_path_entry("ssh_key", "ssh", "ed25519", "m/44'/822'/0'/0/0");
role_table_add(&table, &main_role);
role_table_add(&table, &ssh_role);
mnemonic_init(&mnemonic);
mnemonic_load(&mnemonic, valid_12);
dispatcher_init(&dispatcher, &table, &mnemonic);
/* 1. Valid get_public_key no selector -> default main, not_yet_derived */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}");
check_condition("get_public_key default role returns not_yet_derived",
response_has(resp, "\"id\":\"1\"") && response_has(resp, "\"result\":\"not_yet_derived\""));
free(resp);
/* 2. sign_event with role main */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\"}]}");
check_condition("sign_event with role=main returns stub",
response_has(resp, "\"id\":\"2\"") && response_has(resp, "\"result\":\"stub:sign_event_ok\""));
free(resp);
/* 3. sign_event with nostr_index 0 */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"nostr_index\":0}]}");
check_condition("sign_event with nostr_index=0 returns stub",
response_has(resp, "\"id\":\"3\"") && response_has(resp, "\"result\":\"stub:sign_event_ok\""));
free(resp);
/* 4. ambiguous selector role + nostr_index */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"4\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\",\"nostr_index\":0}]}");
check_condition("ambiguous selector returns 1001",
response_has(resp, "\"id\":\"4\"") && response_has(resp, "\"code\":1001"));
free(resp);
/* 5. role not found */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"5\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"nonexistent\"}]}");
check_condition("role not found returns 1002",
response_has(resp, "\"id\":\"5\"") && response_has(resp, "\"code\":1002"));
free(resp);
/* 6. purpose mismatch: sign_event against ssh role */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"6\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"ssh_key\"}]}");
check_condition("purpose mismatch returns 1004",
response_has(resp, "\"id\":\"6\"") && response_has(resp, "\"code\":1004"));
free(resp);
/* 7. invalid JSON */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"7\",\"method\":\"sign_event\",\"params\":[\"{}\"]");
check_condition("invalid JSON returns -32700",
response_has(resp, "\"code\":-32700"));
free(resp);
/* 8. missing method */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"8\",\"params\":[\"{}\"]}");
check_condition("missing method returns -32600",
response_has(resp, "\"id\":\"8\"") && response_has(resp, "\"code\":-32600"));
free(resp);
/* 9. mnemonic not loaded */
mnemonic_unload(&mnemonic);
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"9\",\"method\":\"get_public_key\",\"params\":[\"\"]}");
check_condition("mnemonic not loaded returns 1006",
response_has(resp, "\"id\":\"9\"") && response_has(resp, "\"code\":1006"));
free(resp);
mnemonic_load(&mnemonic, valid_12);
/* 10. unknown verb via enforcement */
resp = dispatcher_handle_request(&dispatcher,
"{\"id\":\"10\",\"method\":\"foo_bar\",\"params\":[\"\"]}");
check_condition("unknown verb returns -32601",
response_has(resp, "\"id\":\"10\"") && response_has(resp, "\"code\":-32601"));
free(resp);
printf("%d/10 tests passed\n", g_passes);
return (g_passes == 10 && g_total == 10) ? 0 : 1;
}

92
tests/test_enforcement.c Normal file
View File

@@ -0,0 +1,92 @@
#include "../src/enforcement.h"
#include <stdio.h>
#include <string.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static role_entry_t make_role(const char *purpose, const char *curve) {
role_entry_t role;
memset(&role, 0, sizeof(role));
strncpy(role.name, "test_role", sizeof(role.name) - 1);
strncpy(role.purpose_str, purpose, sizeof(role.purpose_str) - 1);
strncpy(role.curve_str, curve, sizeof(role.curve_str) - 1);
role.purpose = role_purpose_from_str(role.purpose_str);
role.curve = role_curve_from_str(role.curve_str);
return role;
}
int main(void) {
role_entry_t nostr_secp = make_role("nostr", "secp256k1");
role_entry_t bitcoin_secp = make_role("bitcoin", "secp256k1");
role_entry_t nostr_ed = make_role("nostr", "ed25519");
role_entry_t ssh_ed = make_role("ssh", "ed25519");
role_entry_t age_x = make_role("age", "x25519");
check_condition(
"sign_event + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_SIGN_EVENT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"get_public_key + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_GET_PUBLIC_KEY, &nostr_secp) == ENFORCE_OK
);
check_condition(
"nip44_encrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NIP44_ENCRYPT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"nip44_decrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NIP44_DECRYPT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"nip04_encrypt + nostr/secp256k1 -> ENFORCE_OK",
enforce_verb_role(VERB_NIP04_ENCRYPT, &nostr_secp) == ENFORCE_OK
);
check_condition(
"sign_event + bitcoin/secp256k1 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_SIGN_EVENT, &bitcoin_secp) == ENFORCE_ERR_PURPOSE
);
check_condition(
"sign_event + nostr/ed25519 -> ENFORCE_ERR_CURVE",
enforce_verb_role(VERB_SIGN_EVENT, &nostr_ed) == ENFORCE_ERR_CURVE
);
check_condition(
"sign_event + ssh/ed25519 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_SIGN_EVENT, &ssh_ed) == ENFORCE_ERR_PURPOSE
);
check_condition(
"unknown_verb + nostr/secp256k1 -> ENFORCE_ERR_UNKNOWN_VERB",
enforce_verb_role("unknown_verb", &nostr_secp) == ENFORCE_ERR_UNKNOWN_VERB
);
check_condition(
"nip44_encrypt + age/x25519 -> ENFORCE_ERR_PURPOSE",
enforce_verb_role(VERB_NIP44_ENCRYPT, &age_x) == ENFORCE_ERR_PURPOSE
);
printf("%d/10 tests passed\n", g_passes);
return (g_passes == 10 && g_total == 10) ? 0 : 1;
}

256
tests/test_integration.c Normal file
View File

@@ -0,0 +1,256 @@
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define SOCKET_NAME "nsigner"
#define MAX_MSG_SIZE 65536
static int g_failures = 0;
static void check_condition(const char *name, int condition) {
if (condition) {
printf("PASS: %s\n", name);
} else {
printf("FAIL: %s\n", name);
g_failures++;
}
}
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 read_full(int fd, void *buf, size_t len) {
unsigned char *p = (unsigned char *)buf;
size_t off = 0;
while (off < len) {
ssize_t n = read(fd, p + off, len - off);
if (n == 0) {
return -1;
}
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -1;
}
off += (size_t)n;
}
return 0;
}
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;
}
static int connect_socket_retry(const char *name, int timeout_ms) {
int elapsed = 0;
while (elapsed < timeout_ms) {
int fd;
struct sockaddr_un addr;
socklen_t addr_len;
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
strncpy(&addr.sun_path[1], 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(name));
if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
return fd;
}
close(fd);
sleep_ms(100);
elapsed += 100;
}
return -1;
}
static int send_framed(int fd, const char *payload) {
uint32_t len = (uint32_t)strlen(payload);
uint32_t be_len = htonl(len);
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
if (write_full(fd, payload, len) != 0) {
return -1;
}
return 0;
}
static int recv_framed(int fd, char **out_payload) {
uint32_t be_len;
uint32_t len;
char *payload;
if (out_payload == NULL) {
return -1;
}
*out_payload = NULL;
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
return -1;
}
len = ntohl(be_len);
if (len == 0 || len > MAX_MSG_SIZE) {
return -1;
}
payload = (char *)malloc((size_t)len + 1U);
if (payload == NULL) {
return -1;
}
if (read_full(fd, payload, len) != 0) {
free(payload);
return -1;
}
payload[len] = '\0';
*out_payload = payload;
return 0;
}
static int request_roundtrip(const char *req, char **resp) {
int fd = connect_socket_retry(SOCKET_NAME, 5000);
int rc;
if (fd < 0) {
return -1;
}
rc = send_framed(fd, req);
if (rc == 0) {
rc = recv_framed(fd, resp);
}
close(fd);
return rc;
}
int main(void) {
int stdin_pipe[2];
pid_t child;
const char *mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\n\n";
char *resp = NULL;
int status;
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) {
int null_fd = open("/dev/null", O_WRONLY);
if (null_fd >= 0) {
dup2(null_fd, STDOUT_FILENO);
dup2(null_fd, STDERR_FILENO);
close(null_fd);
}
dup2(stdin_pipe[0], STDIN_FILENO);
close(stdin_pipe[0]);
close(stdin_pipe[1]);
execl("./build/nsigner", "./build/nsigner", (char *)NULL);
_exit(127);
}
close(stdin_pipe[0]);
if (write_full(stdin_pipe[1], mnemonic, strlen(mnemonic)) == 0) {
check_condition("feed mnemonic to child stdin", 1);
} else {
check_condition("feed mnemonic to child stdin", 0);
}
close(stdin_pipe[1]);
sleep_ms(800);
if (request_roundtrip("{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}", &resp) == 0) {
check_condition("get_public_key response id", strstr(resp, "\"id\":\"1\"") != NULL);
check_condition("get_public_key has result", strstr(resp, "\"result\":") != NULL);
} else {
check_condition("get_public_key request roundtrip", 0);
}
free(resp);
resp = NULL;
if (request_roundtrip("{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{}\",{\"role\":\"main\"}]}", &resp) == 0) {
check_condition("sign_event response id", strstr(resp, "\"id\":\"2\"") != NULL);
check_condition("sign_event stub result", strstr(resp, "stub:sign_event_ok") != NULL);
} else {
check_condition("sign_event request roundtrip", 0);
}
free(resp);
if (kill(child, SIGTERM) == 0) {
check_condition("send SIGTERM to child", 1);
} else {
check_condition("send SIGTERM to child", 0);
}
if (waitpid(child, &status, 0) > 0) {
check_condition("child exited", WIFEXITED(status) || WIFSIGNALED(status));
} else {
check_condition("child exited", 0);
}
if (g_failures == 0) {
printf("ALL TESTS PASSED\n");
return 0;
}
printf("TESTS FAILED: %d\n", g_failures);
return 1;
}

83
tests/test_mnemonic.c Normal file
View File

@@ -0,0 +1,83 @@
#include "../src/mnemonic.h"
#include <stdio.h>
#include <string.h>
static int g_failures = 0;
/* Print PASS/FAIL and track failures. */
static void check_condition(const char *name, int condition) {
if (condition) {
printf("PASS: %s\n", name);
} else {
printf("FAIL: %s\n", name);
g_failures++;
}
}
/* Build a deterministic 25-word invalid mnemonic in `out`. */
static void build_25_word_phrase(char *out, size_t out_size) {
size_t used = 0;
int i;
if (out == NULL || out_size == 0) {
return;
}
out[0] = '\0';
for (i = 0; i < 25; ++i) {
const char *word = "abandon";
int wrote;
wrote = snprintf(out + used, out_size - used, "%s%s", (i == 0) ? "" : " ", word);
if (wrote < 0 || (size_t)wrote >= out_size - used) {
/* Truncate safely and stop if buffer is insufficient. */
out[out_size - 1] = '\0';
return;
}
used += (size_t)wrote;
}
}
int main(void) {
mnemonic_state_t state;
const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
const char *invalid_11 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
char invalid_25[MNEMONIC_MAX_LEN];
const char *loaded_phrase;
int rc;
mnemonic_init(&state);
check_condition("state initially unloaded", mnemonic_is_loaded(&state) == 0);
rc = mnemonic_load(&state, valid_12);
check_condition("load valid 12-word mnemonic returns 0", rc == 0);
check_condition("state loaded after valid load", mnemonic_is_loaded(&state) == 1);
check_condition("word_count == 12", state.word_count == 12);
loaded_phrase = mnemonic_get_phrase(&state);
check_condition("mnemonic_get_phrase non-NULL when loaded", loaded_phrase != NULL);
check_condition("mnemonic_get_phrase matches input", loaded_phrase != NULL && strcmp(loaded_phrase, valid_12) == 0);
mnemonic_unload(&state);
check_condition("state unloaded after mnemonic_unload", mnemonic_is_loaded(&state) == 0);
check_condition("mnemonic_get_phrase NULL after unload", mnemonic_get_phrase(&state) == NULL);
rc = mnemonic_load(&state, invalid_11);
check_condition("reject invalid 11-word mnemonic", rc == -1);
check_condition("state remains unloaded after invalid 11-word load", mnemonic_is_loaded(&state) == 0);
build_25_word_phrase(invalid_25, sizeof(invalid_25));
rc = mnemonic_load(&state, invalid_25);
check_condition("reject invalid 25-word mnemonic", rc == -1);
check_condition("state remains unloaded after invalid 25-word load", mnemonic_is_loaded(&state) == 0);
if (g_failures == 0) {
printf("ALL TESTS PASSED\n");
return 0;
}
printf("TESTS FAILED: %d\n", g_failures);
return 1;
}

127
tests/test_policy.c Normal file
View File

@@ -0,0 +1,127 @@
#include "../src/policy.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static void add_single_entry(policy_table_t *table,
const char *caller,
const char *verb,
const char *role,
const char *purpose,
prompt_mode_t prompt) {
policy_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.caller, caller, sizeof(e.caller) - 1);
if (verb != NULL) {
strncpy(e.verbs[0], verb, sizeof(e.verbs[0]) - 1);
e.verb_count = 1;
}
if (role != NULL) {
strncpy(e.roles[0], role, sizeof(e.roles[0]) - 1);
e.role_count = 1;
}
if (purpose != NULL) {
strncpy(e.purposes[0], purpose, sizeof(e.purposes[0]) - 1);
e.purpose_count = 1;
}
e.prompt = prompt;
policy_table_add(table, &e);
}
int main(void) {
policy_table_t table;
int rc;
uid_t uid = getuid();
char same_uid[64];
char other_uid[64];
(void)snprintf(same_uid, sizeof(same_uid), "uid:%u", (unsigned int)uid);
(void)snprintf(other_uid, sizeof(other_uid), "uid:%u", (unsigned int)(uid + 1U));
policy_init_default(&table, uid);
rc = policy_check(&table, same_uid, "sign_event", "main", "nostr");
check_condition("policy_init_default allows same uid", rc == POLICY_ALLOW);
rc = policy_check(&table, other_uid, "sign_event", "main", "nostr");
check_condition("policy_init_default denies different uid", rc == POLICY_DENY);
/* Exact caller, verb, role, purpose + never => allow */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("exact match returns POLICY_ALLOW", rc == POLICY_ALLOW);
/* Wildcard caller + deny => deny */
policy_table_init(&table);
add_single_entry(&table, "*", "sign_event", "main", "nostr", PROMPT_DENY);
rc = policy_check(&table, "uid:2000", "sign_event", "main", "nostr");
check_condition("wildcard caller with deny returns POLICY_DENY", rc == POLICY_DENY);
/* Caller no match => POLICY_NO_MATCH */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:9999", "sign_event", "main", "nostr");
check_condition("caller mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Verb not in list => no match */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "get_public_key", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("verb mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Role not in list => no match */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "throwaway", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("role mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Purpose not in list => no match */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "bitcoin", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("purpose mismatch returns POLICY_NO_MATCH", rc == POLICY_NO_MATCH);
/* Empty verbs list means all verbs */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", NULL, "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "nip44_encrypt", "main", "nostr");
check_condition("empty verbs list matches any verb", rc == POLICY_ALLOW);
/* prompt=first_per_boot => POLICY_PROMPT */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_FIRST_PER_BOOT);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("first_per_boot returns POLICY_PROMPT", rc == POLICY_PROMPT);
/* prompt=every_request => POLICY_PROMPT */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_EVERY_REQUEST);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("every_request returns POLICY_PROMPT", rc == POLICY_PROMPT);
/* First matching entry wins */
policy_table_init(&table);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_DENY);
add_single_entry(&table, "uid:1000", "sign_event", "main", "nostr", PROMPT_NEVER);
rc = policy_check(&table, "uid:1000", "sign_event", "main", "nostr");
check_condition("first matching entry wins", rc == POLICY_DENY);
printf("%d/%d tests passed\n", g_passes, g_total);
return (g_passes == g_total) ? 0 : 1;
}

135
tests/test_role_table.c Normal file
View File

@@ -0,0 +1,135 @@
#include "../src/role_table.h"
#include <stdio.h>
#include <string.h>
static int g_failures = 0;
static void check_condition(const char *name, int condition) {
if (condition) {
printf("PASS: %s\n", name);
} else {
printf("FAIL: %s\n", name);
g_failures++;
}
}
static role_entry_t make_nostr_entry(const char *name, const char *purpose, const char *curve, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, purpose, sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, curve, sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_path_entry(const char *name, const char *purpose, const char *curve, const char *path) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, purpose, sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, curve, sizeof(e.curve_str) - 1);
strncpy(e.role_path, path, sizeof(e.role_path) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_ROLE_PATH;
e.nostr_index = -1;
e.derived = 0;
return e;
}
int main(void) {
role_table_t table;
role_entry_t e_main;
role_entry_t e_throwaway;
role_entry_t e_btc;
role_entry_t e_ssh;
role_entry_t *found;
int rc;
int i;
role_table_init(&table);
check_condition("init count==0", table.count == 0);
e_main = make_nostr_entry("main", "nostr", "secp256k1", 0);
rc = role_table_add(&table, &e_main);
check_condition("add main returns 0", rc == 0);
e_throwaway = make_nostr_entry("throwaway", "nostr", "secp256k1", 1);
rc = role_table_add(&table, &e_throwaway);
check_condition("add throwaway returns 0", rc == 0);
e_btc = make_path_entry("btc_savings", "bitcoin", "secp256k1", "m/84'/0'/0'/0/0");
rc = role_table_add(&table, &e_btc);
check_condition("add btc_savings returns 0", rc == 0);
e_ssh = make_path_entry("ssh_key", "ssh", "ed25519", "m/44'/822'/0'/0/0");
rc = role_table_add(&table, &e_ssh);
check_condition("add ssh_key returns 0", rc == 0);
found = role_table_find_by_name(&table, "main");
check_condition("find_by_name(main) not NULL", found != NULL);
check_condition("find_by_name(main) purpose nostr", found != NULL && found->purpose == PURPOSE_NOSTR);
found = role_table_find_by_nostr_index(&table, 1);
check_condition("find_by_nostr_index(1) is throwaway", found != NULL && strcmp(found->name, "throwaway") == 0);
found = role_table_find_by_path(&table, "m/84'/0'/0'/0/0");
check_condition("find_by_path btc path is btc_savings", found != NULL && strcmp(found->name, "btc_savings") == 0);
found = role_table_get_default(&table);
check_condition("get_default() is main", found != NULL && strcmp(found->name, "main") == 0);
rc = role_table_add(&table, &e_main);
check_condition("duplicate name rejection returns -2", rc == -2);
role_table_init(&table);
for (i = 0; i < ROLE_TABLE_MAX_ENTRIES; ++i) {
role_entry_t e;
char name_buf[ROLE_NAME_MAX];
memset(&e, 0, sizeof(e));
snprintf(name_buf, sizeof(name_buf), "role_%d", i);
strncpy(e.name, name_buf, sizeof(e.name) - 1);
strncpy(e.purpose_str, "nostr", sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, "secp256k1", sizeof(e.curve_str) - 1);
e.purpose = PURPOSE_NOSTR;
e.curve = CURVE_SECP256K1;
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = i;
rc = role_table_add(&table, &e);
check_condition("fill table role add returns 0", rc == 0);
}
{
role_entry_t overflow;
memset(&overflow, 0, sizeof(overflow));
strncpy(overflow.name, "overflow", sizeof(overflow.name) - 1);
strncpy(overflow.purpose_str, "nostr", sizeof(overflow.purpose_str) - 1);
strncpy(overflow.curve_str, "secp256k1", sizeof(overflow.curve_str) - 1);
overflow.purpose = PURPOSE_NOSTR;
overflow.curve = CURVE_SECP256K1;
overflow.selector_type = SELECTOR_NOSTR_INDEX;
overflow.nostr_index = 999;
rc = role_table_add(&table, &overflow);
check_condition("table full rejection returns -1", rc == -1);
}
if (g_failures == 0) {
printf("ALL TESTS PASSED\n");
return 0;
}
printf("TESTS FAILED: %d\n", g_failures);
return 1;
}

170
tests/test_selector.c Normal file
View File

@@ -0,0 +1,170 @@
#include "../src/selector.h"
#include <stdio.h>
#include <string.h>
static int g_passes = 0;
static int g_total = 0;
static void check_condition(const char *name, int condition) {
g_total++;
if (condition) {
printf("PASS: %s\n", name);
g_passes++;
} else {
printf("FAIL: %s\n", name);
}
}
static role_entry_t make_nostr_entry(const char *name, const char *purpose, const char *curve, int idx) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, purpose, sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, curve, sizeof(e.curve_str) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_NOSTR_INDEX;
e.nostr_index = idx;
e.derived = 0;
return e;
}
static role_entry_t make_path_entry(const char *name, const char *purpose, const char *curve, const char *path) {
role_entry_t e;
memset(&e, 0, sizeof(e));
strncpy(e.name, name, sizeof(e.name) - 1);
strncpy(e.purpose_str, purpose, sizeof(e.purpose_str) - 1);
strncpy(e.curve_str, curve, sizeof(e.curve_str) - 1);
strncpy(e.role_path, path, sizeof(e.role_path) - 1);
e.purpose = role_purpose_from_str(e.purpose_str);
e.curve = role_curve_from_str(e.curve_str);
e.selector_type = SELECTOR_ROLE_PATH;
e.nostr_index = -1;
e.derived = 0;
return e;
}
static void build_standard_table(role_table_t *table) {
role_entry_t e_main;
role_entry_t e_throwaway;
role_entry_t e_btc;
role_table_init(table);
e_main = make_nostr_entry("main", "nostr", "secp256k1", 0);
e_throwaway = make_nostr_entry("throwaway", "nostr", "secp256k1", 1);
e_btc = make_path_entry("btc_savings", "bitcoin", "secp256k1", "m/84'/0'/0'/0/0");
role_table_add(table, &e_main);
role_table_add(table, &e_throwaway);
role_table_add(table, &e_btc);
}
int main(void) {
role_table_t table;
role_table_t no_main_table;
selector_request_t req;
role_entry_t *out;
int rc;
build_standard_table(&table);
role_table_init(&no_main_table);
{
role_entry_t e_throwaway = make_nostr_entry("throwaway", "nostr", "secp256k1", 1);
role_entry_t e_btc = make_path_entry("btc_savings", "bitcoin", "secp256k1", "m/84'/0'/0'/0/0");
role_table_add(&no_main_table, &e_throwaway);
role_table_add(&no_main_table, &e_btc);
}
/* 1. No selector given, "main" exists -> resolves to "main" */
selector_request_init(&req);
out = NULL;
rc = selector_resolve(&req, &table, &out);
check_condition("default resolves to main", rc == SELECTOR_OK && out != NULL && strcmp(out->name, "main") == 0);
/* 2. No selector given, no "main" role -> SELECTOR_ERR_NO_DEFAULT */
selector_request_init(&req);
out = (role_entry_t *)0x1;
rc = selector_resolve(&req, &no_main_table, &out);
check_condition("no default role returns SELECTOR_ERR_NO_DEFAULT", rc == SELECTOR_ERR_NO_DEFAULT && out == NULL);
/* 3. role = "throwaway" -> resolves to "throwaway" */
selector_request_init(&req);
req.has_role = 1;
strncpy(req.role_name, "throwaway", sizeof(req.role_name) - 1);
out = NULL;
rc = selector_resolve(&req, &table, &out);
check_condition("role selector resolves throwaway", rc == SELECTOR_OK && out != NULL && strcmp(out->name, "throwaway") == 0);
/* 4. nostr_index = 1 -> resolves to nostr_index == 1 */
selector_request_init(&req);
req.has_nostr_index = 1;
req.nostr_index = 1;
out = NULL;
rc = selector_resolve(&req, &table, &out);
check_condition("nostr_index selector resolves index 1", rc == SELECTOR_OK && out != NULL && out->nostr_index == 1);
/* 5. role_path = m/84'/0'/0'/0/0 -> resolves to btc_savings */
selector_request_init(&req);
req.has_role_path = 1;
strncpy(req.role_path, "m/84'/0'/0'/0/0", sizeof(req.role_path) - 1);
out = NULL;
rc = selector_resolve(&req, &table, &out);
check_condition("role_path selector resolves btc_savings", rc == SELECTOR_OK && out != NULL && strcmp(out->name, "btc_savings") == 0);
/* 6. role + nostr_index both set -> SELECTOR_ERR_AMBIGUOUS */
selector_request_init(&req);
req.has_role = 1;
strncpy(req.role_name, "main", sizeof(req.role_name) - 1);
req.has_nostr_index = 1;
req.nostr_index = 0;
out = (role_entry_t *)0x1;
rc = selector_resolve(&req, &table, &out);
check_condition("role and nostr_index set is ambiguous", rc == SELECTOR_ERR_AMBIGUOUS && out == NULL);
/* 7. role = nonexistent -> SELECTOR_ERR_NOT_FOUND */
selector_request_init(&req);
req.has_role = 1;
strncpy(req.role_name, "nonexistent", sizeof(req.role_name) - 1);
out = (role_entry_t *)0x1;
rc = selector_resolve(&req, &table, &out);
check_condition("unknown role returns SELECTOR_ERR_NOT_FOUND", rc == SELECTOR_ERR_NOT_FOUND && out == NULL);
/* 8. nostr_index = 99 -> SELECTOR_ERR_NOT_FOUND */
selector_request_init(&req);
req.has_nostr_index = 1;
req.nostr_index = 99;
out = (role_entry_t *)0x1;
rc = selector_resolve(&req, &table, &out);
check_condition("unknown nostr_index returns SELECTOR_ERR_NOT_FOUND", rc == SELECTOR_ERR_NOT_FOUND && out == NULL);
/* 9. role_path = m/99'/0'/0' -> SELECTOR_ERR_NOT_FOUND */
selector_request_init(&req);
req.has_role_path = 1;
strncpy(req.role_path, "m/99'/0'/0'", sizeof(req.role_path) - 1);
out = (role_entry_t *)0x1;
rc = selector_resolve(&req, &table, &out);
check_condition("unknown role_path returns SELECTOR_ERR_NOT_FOUND", rc == SELECTOR_ERR_NOT_FOUND && out == NULL);
/* 10. all three selectors set -> SELECTOR_ERR_AMBIGUOUS */
selector_request_init(&req);
req.has_role = 1;
strncpy(req.role_name, "main", sizeof(req.role_name) - 1);
req.has_nostr_index = 1;
req.nostr_index = 0;
req.has_role_path = 1;
strncpy(req.role_path, "m/84'/0'/0'/0/0", sizeof(req.role_path) - 1);
out = (role_entry_t *)0x1;
rc = selector_resolve(&req, &table, &out);
check_condition("all selectors set is ambiguous", rc == SELECTOR_ERR_AMBIGUOUS && out == NULL);
printf("%d/10 tests passed\n", g_passes);
return (g_passes == 10 && g_total == 10) ? 0 : 1;
}