From ee0c60c3432a5b9a57a49e21198f42331ea0ec87 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sat, 2 May 2026 14:11:51 -0400 Subject: [PATCH] v0.0.4 - Fix interactive smoke test JSON escaping and socket-name client invocation order --- .gitignore | 2 + src/main.c | 4 +- tests/test.sh | 87 +++++ tests/test_dispatcher.c | 609 ---------------------------------- tests/test_enforcement.c | 532 ----------------------------- tests/test_integration.c | 698 --------------------------------------- tests/test_mnemonic.c | 541 ------------------------------ tests/test_policy.c | 567 ------------------------------- tests/test_role_table.c | 575 -------------------------------- tests/test_selector.c | 610 ---------------------------------- tests/test_socket_name.c | 491 --------------------------- 11 files changed, 91 insertions(+), 4625 deletions(-) create mode 100755 tests/test.sh delete mode 100644 tests/test_dispatcher.c delete mode 100644 tests/test_enforcement.c delete mode 100644 tests/test_integration.c delete mode 100644 tests/test_mnemonic.c delete mode 100644 tests/test_policy.c delete mode 100644 tests/test_role_table.c delete mode 100644 tests/test_selector.c delete mode 100644 tests/test_socket_name.c diff --git a/.gitignore b/.gitignore index 21d0e3d..e3e5e94 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build/ .gitea_token resources/ .test_mnemonic +Trash/ + diff --git a/src/main.c b/src/main.c index 448340b..be8d7ed 100644 --- a/src/main.c +++ b/src/main.c @@ -436,8 +436,8 @@ int socket_name_random(char *out, size_t out_len); /* Version information (auto-updated by build/version tooling) */ #define NSIGNER_VERSION_MAJOR 0 #define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 3 -#define NSIGNER_VERSION "v0.0.3" +#define NSIGNER_VERSION_PATCH 4 +#define NSIGNER_VERSION "v0.0.4" /* NSIGNER_HEADERLESS_DECLS_END */ diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..7caf0e1 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +NSIGNER_BIN="${NSIGNER_BIN:-./build/nsigner}" + +if [[ ! -x "$NSIGNER_BIN" ]]; then + echo "[error] nsigner binary not found or not executable at: $NSIGNER_BIN" + echo " Build it first with: make dev" + exit 1 +fi + +echo "n_signer interactive smoke test" +echo "Using binary: $NSIGNER_BIN" +echo + +default_socket="" +if [[ -r /proc/net/unix ]]; then + default_socket="$(awk '/@nsigner_/ {sub(/^@/, "", $8); print $8; exit}' /proc/net/unix || true)" +fi + +if [[ -n "$default_socket" ]]; then + read -r -p "Signer socket name (without @) [${default_socket}]: " socket_name + socket_name="${socket_name:-$default_socket}" +else + read -r -p "Signer socket name (without @, e.g. nsigner_hairy_dog): " socket_name +fi + +if [[ -z "${socket_name}" ]]; then + echo "[error] socket name is required" + exit 1 +fi + +run_request() { + local req="$1" + "$NSIGNER_BIN" --socket-name "$socket_name" client "$req" || true +} + +echo +echo "[1/5] get_public_key" +req_get='{"id":"1","method":"get_public_key","params":[""]}' +resp_get="$(run_request "$req_get")" +echo "request : $req_get" +echo "response: $resp_get" +pubkey="$(printf '%s' "$resp_get" | sed -n 's/.*"result":"\([0-9a-fA-F]\{64\}\)".*/\1/p')" +if [[ -n "$pubkey" ]]; then + echo "pubkey : $pubkey" +else + echo "[warn] could not parse pubkey from response" +fi + +echo +echo "[2/5] sign_event" +now_ts="$(date +%s)" +event_json='{"kind":1,"content":"hello from tests/test.sh","tags":[],"created_at":__TS__}' +event_json="${event_json/__TS__/${now_ts}}" +escaped_event_json="${event_json//\"/\\\"}" +req_sign="{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"role\":\"main\"}]}" +resp_sign="$(run_request "$req_sign")" +echo "request : $req_sign" +echo "response: $resp_sign" +if printf '%s' "$resp_sign" | grep -q '"result"'; then + echo "[ok] sign_event returned result" +else + echo "[warn] sign_event did not return result" +fi + +echo +echo "[3/5] sign_event with nostr_index=0" +req_sign_idx="{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"${escaped_event_json}\",{\"nostr_index\":0}]}" +resp_sign_idx="$(run_request "$req_sign_idx")" +echo "request : $req_sign_idx" +echo "response: $resp_sign_idx" + +echo +echo "[4/5] role selector error case (unknown role)" +req_bad_role='{"id":"4","method":"sign_event","params":["{}",{"role":"does_not_exist"}]}' +resp_bad_role="$(run_request "$req_bad_role")" +echo "request : $req_bad_role" +echo "response: $resp_bad_role" + +echo +echo "[5/5] list currently running signers" +list_resp="$($NSIGNER_BIN list || true)" +echo "$list_resp" + +echo +echo "Smoke test complete." \ No newline at end of file diff --git a/tests/test_dispatcher.c b/tests/test_dispatcher.c deleted file mode 100644 index 5edc357..0000000 --- a/tests/test_dispatcher.c +++ /dev/null @@ -1,609 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include - -#include -#include -#include - -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; - key_store_t key_store; - const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - char *resp; - int derived; - - 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); - - memset(&key_store, 0, sizeof(key_store)); - dispatcher_init(&dispatcher, &table, &mnemonic, &key_store); - - derived = nostr_init(); - check_condition("nostr_init succeeds", derived == 0); - - derived = crypto_derive_all(&key_store, &table, &mnemonic); - check_condition("crypto_derive_all derives at least one key", derived >= 1); - - /* 1. Valid get_public_key no selector -> default main, real pubkey */ - resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"1\",\"method\":\"get_public_key\",\"params\":[\"\"]}"); - check_condition("get_public_key default role returns derived hex", - response_has(resp, "\"id\":\"1\"") && !response_has(resp, "not_yet_derived") && response_has(resp, "\"result\":\"")); - free(resp); - - /* 2. sign_event with role main */ - resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"2\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[]}\",{\"role\":\"main\"}]}"); - check_condition("sign_event with role=main returns signed event", - response_has(resp, "\"id\":\"2\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at")); - free(resp); - - /* 3. sign_event with nostr_index 0 */ - resp = dispatcher_handle_request(&dispatcher, - "{\"id\":\"3\",\"method\":\"sign_event\",\"params\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello2\\\",\\\"tags\\\":[]}\",{\"nostr_index\":0}]}"); - check_condition("sign_event with nostr_index=0 returns signed event", - response_has(resp, "\"id\":\"3\"") && response_has(resp, "pubkey") && response_has(resp, "sig") && response_has(resp, "created_at")); - 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); - - crypto_wipe(&key_store); - nostr_cleanup(); - - printf("%d/12 tests passed\n", g_passes); - - return (g_passes == 12 && g_total == 12) ? 0 : 1; -} diff --git a/tests/test_enforcement.c b/tests/test_enforcement.c deleted file mode 100644 index 12b3546..0000000 --- a/tests/test_enforcement.c +++ /dev/null @@ -1,532 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include - -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; -} diff --git a/tests/test_integration.c b/tests/test_integration.c deleted file mode 100644 index c238458..0000000 --- a/tests/test_integration.c +++ /dev/null @@ -1,698 +0,0 @@ -#define _GNU_SOURCE - -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define SOCKET_NAME "nsigner_test_run" -#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 = "\nabandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\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", "--socket-name", SOCKET_NAME, (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\":[\"{\\\"kind\\\":1,\\\"content\\\":\\\"hello\\\",\\\"tags\\\":[],\\\"created_at\\\":1700000000}\",{\"role\":\"main\"}]}", &resp) == 0) { - check_condition("sign_event response id", strstr(resp, "\"id\":\"2\"") != NULL); - check_condition("sign_event has result", strstr(resp, "\"result\":") != 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; -} diff --git a/tests/test_mnemonic.c b/tests/test_mnemonic.c deleted file mode 100644 index 275e6d2..0000000 --- a/tests/test_mnemonic.c +++ /dev/null @@ -1,541 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include - -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; - char generated_12[MNEMONIC_MAX_LEN]; - char generated_24[MNEMONIC_MAX_LEN]; - 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); - - rc = mnemonic_generate(12, generated_12, sizeof(generated_12)); - check_condition("mnemonic_generate(12) returns 0", rc == 0); - check_condition("mnemonic_generate(12) output non-empty", rc == 0 && generated_12[0] != '\0'); - check_condition("load generated 12-word mnemonic", rc == 0 && mnemonic_load(&state, generated_12) == 0); - check_condition("generated 12 word_count == 12", mnemonic_is_loaded(&state) && state.word_count == 12); - mnemonic_unload(&state); - - rc = mnemonic_generate(24, generated_24, sizeof(generated_24)); - check_condition("mnemonic_generate(24) returns 0", rc == 0); - check_condition("mnemonic_generate(24) output non-empty", rc == 0 && generated_24[0] != '\0'); - check_condition("load generated 24-word mnemonic", rc == 0 && mnemonic_load(&state, generated_24) == 0); - check_condition("generated 24 word_count == 24", mnemonic_is_loaded(&state) && state.word_count == 24); - mnemonic_unload(&state); - - check_condition("two generated mnemonics differ", strcmp(generated_12, generated_24) != 0); - - if (g_failures == 0) { - printf("ALL TESTS PASSED\n"); - return 0; - } - - printf("TESTS FAILED: %d\n", g_failures); - return 1; -} diff --git a/tests/test_policy.c b/tests/test_policy.c deleted file mode 100644 index 2f94ea5..0000000 --- a/tests/test_policy.c +++ /dev/null @@ -1,567 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include -#include - -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; -} diff --git a/tests/test_role_table.c b/tests/test_role_table.c deleted file mode 100644 index 47601b3..0000000 --- a/tests/test_role_table.c +++ /dev/null @@ -1,575 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include - -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; -} diff --git a/tests/test_selector.c b/tests/test_selector.c deleted file mode 100644 index 57db387..0000000 --- a/tests/test_selector.c +++ /dev/null @@ -1,610 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include - -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; -} diff --git a/tests/test_socket_name.c b/tests/test_socket_name.c deleted file mode 100644 index 036f9d5..0000000 --- a/tests/test_socket_name.c +++ /dev/null @@ -1,491 +0,0 @@ -/* NSIGNER_HEADERLESS_DECLS_BEGIN */ -#include -#include -#include -#include - -/* from secure_mem.h */ - - -/* - * Secure memory buffer — mlock'd, zeroized on free. - * Used for mnemonic phrases, private keys, and any sensitive material. - */ -typedef struct { - void *data; /* pointer to locked allocation */ - size_t size; /* usable size in bytes */ - int locked; /* 1 if mlock succeeded */ -} secure_buf_t; - -/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ -int secure_buf_alloc(secure_buf_t *buf, size_t size); - -/* Zeroize and free a secure buffer. Always succeeds (idempotent). */ -void secure_buf_free(secure_buf_t *buf); - -/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ -void secure_memzero(void *ptr, size_t len); - - -/* from mnemonic.h */ - - -/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */ -#define MNEMONIC_MAX_LEN 256 - -/* - * Mnemonic state — holds the loaded mnemonic in secure memory. - * Only one mnemonic is active at a time per process. - */ -typedef struct { - secure_buf_t buf; /* secure storage for the mnemonic string */ - int loaded; /* 1 if a mnemonic is currently loaded */ - int word_count; /* 12, 15, 18, 21, or 24 */ -} mnemonic_state_t; - -/* Initialize mnemonic state (must be called before use). */ -void mnemonic_init(mnemonic_state_t *state); - -/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24). - * Returns 0 on success, -1 on invalid input, -2 on memory error. */ -int mnemonic_load(mnemonic_state_t *state, const char *phrase); - -/* Zeroize and unload the mnemonic. Idempotent. */ -void mnemonic_unload(mnemonic_state_t *state); - -/* Check if a mnemonic is currently loaded. */ -int mnemonic_is_loaded(const mnemonic_state_t *state); - -/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */ -const char *mnemonic_get_phrase(const mnemonic_state_t *state); - -/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out. - * Returns 0 on success, -1 on invalid arguments or generation failure. */ -int mnemonic_generate(int word_count, char *out, size_t out_len); - - -/* from role_table.h */ - - -/* Maximum limits */ -#define ROLE_NAME_MAX 64 -#define ROLE_PATH_MAX 128 -#define ROLE_PURPOSE_MAX 32 -#define ROLE_CURVE_MAX 16 -#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */ -#define ROLE_TABLE_MAX_ENTRIES 64 - -/* Purpose enum for fast comparison (string form kept for config/display) */ -typedef enum { - PURPOSE_NOSTR = 0, - PURPOSE_BITCOIN, - PURPOSE_SSH, - PURPOSE_AGE, - PURPOSE_FIPS, - PURPOSE_UNKNOWN -} role_purpose_t; - -/* Curve enum */ -typedef enum { - CURVE_SECP256K1 = 0, - CURVE_ED25519, - CURVE_X25519, - CURVE_UNKNOWN -} role_curve_t; - -/* Selector type — how this role's key is addressed */ -typedef enum { - SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */ - SELECTOR_ROLE_PATH /* uses explicit full path */ -} role_selector_type_t; - -/* A single role entry */ -typedef struct { - char name[ROLE_NAME_MAX]; - char purpose_str[ROLE_PURPOSE_MAX]; - char curve_str[ROLE_CURVE_MAX]; - role_purpose_t purpose; - role_curve_t curve; - role_selector_type_t selector_type; - int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */ - char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */ - char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */ - int derived; /* 1 if pubkey_hex has been populated */ -} role_entry_t; - -/* The role table */ -typedef struct { - role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; - int count; -} role_table_t; - -/* Initialize an empty role table */ -void role_table_init(role_table_t *table); - -/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */ -int role_table_add(role_table_t *table, const role_entry_t *entry); - -/* Find a role by name. Returns pointer to entry or NULL. */ -role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); - -/* Find a role by nostr_index. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); - -/* Find a role by role_path. Returns pointer or NULL. */ -role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); - -/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */ -role_entry_t *role_table_get_default(role_table_t *table); - -/* Parse purpose string to enum */ -role_purpose_t role_purpose_from_str(const char *s); - -/* Parse curve string to enum */ -role_curve_t role_curve_from_str(const char *s); - -/* Purpose enum to string */ -const char *role_purpose_to_str(role_purpose_t p); - -/* Curve enum to string */ -const char *role_curve_to_str(role_curve_t c); - - -/* from selector.h */ - - -/* Error codes for selector resolution */ -#define SELECTOR_OK 0 -#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */ -#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */ -#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */ - -/* Parsed selector from a request's options object */ -typedef struct { - int has_role; /* 1 if "role" field was present */ - char role_name[ROLE_NAME_MAX]; - - int has_nostr_index; /* 1 if "nostr_index" field was present */ - int nostr_index; - - int has_role_path; /* 1 if "role_path" field was present */ - char role_path[ROLE_PATH_MAX]; -} selector_request_t; - -/* Initialize a selector request (all fields zeroed/unset) */ -void selector_request_init(selector_request_t *req); - -/* - * Resolve a selector request against the role table. - * On success (returns SELECTOR_OK), *out points to the matched role_entry_t. - * On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL. - */ -int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); - -/* - * Return a human-readable error string for a selector error code. - */ -const char *selector_strerror(int err); - - -/* from enforcement.h */ - - -/* Error codes */ -#define ENFORCE_OK 0 -#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */ -#define ENFORCE_ERR_CURVE -2 /* curve mismatch */ -#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */ - -/* Known verbs */ -#define VERB_SIGN_EVENT "sign_event" -#define VERB_GET_PUBLIC_KEY "get_public_key" -#define VERB_NIP44_ENCRYPT "nip44_encrypt" -#define VERB_NIP44_DECRYPT "nip44_decrypt" -#define VERB_NIP04_ENCRYPT "nip04_encrypt" -#define VERB_NIP04_DECRYPT "nip04_decrypt" - -/* - * Check whether `verb` is allowed to execute against `role`. - * Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code. - * - * The enforcement rules are: - * - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require: - * purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1 - * - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed). - */ -int enforce_verb_role(const char *verb, const role_entry_t *role); - -/* - * Return a human-readable error string for an enforcement error code. - */ -const char *enforce_strerror(int err); - - -/* from policy.h */ - - -#define POLICY_MAX_ENTRIES 32 -#define POLICY_MAX_VERBS 16 -#define POLICY_MAX_ROLES 16 -#define POLICY_MAX_PURPOSES 8 -#define POLICY_VERB_MAX_LEN 32 -#define POLICY_CALLER_MAX_LEN 64 - -/* Prompt behavior */ -typedef enum { - PROMPT_NEVER = 0, - PROMPT_FIRST_PER_BOOT, - PROMPT_EVERY_REQUEST, - PROMPT_DENY -} prompt_mode_t; - -/* A single policy entry */ -typedef struct { - char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */ - char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN]; - int verb_count; - char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX]; - int role_count; - char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX]; - int purpose_count; - prompt_mode_t prompt; -} policy_entry_t; - -/* Policy table */ -typedef struct { - policy_entry_t entries[POLICY_MAX_ENTRIES]; - int count; -} policy_table_t; - -/* Policy check result */ -#define POLICY_ALLOW 0 -#define POLICY_DENY -1 -#define POLICY_PROMPT -2 /* would need user confirmation */ -#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */ - -/* Initialize policy table */ -void policy_table_init(policy_table_t *table); - -/* Initialize default policy: allow same-uid, deny others */ -void policy_init_default(policy_table_t *table, uid_t owner_uid); - -/* Add a policy entry. Returns 0 on success, -1 if full. */ -int policy_table_add(policy_table_t *table, const policy_entry_t *entry); - -/* - * Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`. - * Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. - */ -int policy_check(const policy_table_t *table, const char *caller_id, - const char *verb, const char *role_name, const char *purpose); - -/* Parse prompt mode from string */ -prompt_mode_t prompt_mode_from_str(const char *s); - -/* Prompt mode to string */ -const char *prompt_mode_to_str(prompt_mode_t m); - - -/* from crypto.h */ - - -/* Per-role derived key material (stored in secure memory) */ -typedef struct { - secure_buf_t private_key; /* 32 bytes, mlock'd */ - unsigned char public_key[32]; - char pubkey_hex[65]; /* 64 hex chars + null */ - char npub[128]; /* bech32 npub */ - int valid; -} derived_key_t; - -/* Key store — holds derived keys for all roles */ -typedef struct { - derived_key_t keys[ROLE_TABLE_MAX_ENTRIES]; - int count; -} key_store_t; - -/* Derive keys for all roles in the table using the loaded mnemonic. - * Populates key_store and sets role->pubkey_hex and role->derived for each role. - * Only derives for roles with purpose=nostr and curve=secp256k1 (for now). - * Returns number of keys derived, or -1 on error. */ -int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); - -/* Get the derived private key for a role (by table index). Returns NULL if not derived. */ -const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index); - -/* Get the derived public key hex for a role. Returns NULL if not derived. */ -const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index); - -/* Sign a Nostr event. event_json is the unsigned event JSON string. - * Returns a newly-allocated string containing the signed event JSON, or NULL on error. - * Caller must free() the returned string. */ -char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json); - -/* Zeroize all derived keys in the store. */ -void crypto_wipe(key_store_t *store); - - -/* from dispatcher.h */ - - -/* Dispatcher context — holds references to shared state */ -typedef struct { - role_table_t *role_table; - mnemonic_state_t *mnemonic; - key_store_t *key_store; -} dispatcher_ctx_t; - -/* Initialize dispatcher context */ -void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store); - -/* - * Process a JSON-RPC request string and produce a JSON-RPC response string. - * - * The caller owns the returned string and must free() it. - * Returns NULL only on catastrophic allocation failure. - * - * Response format on success: - * { "id": "...", "result": "..." } - * - * Response format on error: - * { "id": "...", "error": { "code": , "message": "..." } } - * - * Error codes: - * -32700 Parse error (invalid JSON) - * -32600 Invalid request (missing id/method/params) - * -32601 Method not found (unknown verb after enforcement) - * -32602 Invalid params - * 1001 ambiguous_role_selector - * 1002 role_not_found - * 1003 no_default_role - * 1004 purpose_mismatch - * 1005 curve_mismatch - * 1006 mnemonic_not_loaded - */ -char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request); - - -/* from server.h */ - - -#define SERVER_SOCKET_NAME_MAX 108 -#define SERVER_MAX_MSG_SIZE 65536 - -/* Caller identity */ -typedef struct { - uid_t uid; - gid_t gid; - pid_t pid; - char caller_id[64]; /* "uid:" */ -} caller_identity_t; - -/* Server context */ -typedef struct { - char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ - char last_error[256]; - int listen_fd; - int running; - dispatcher_ctx_t *dispatcher; - policy_table_t *policy; - int socket_name_explicit; -} server_ctx_t; - -/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). - * socket_name_explicit should be non-zero when provided via --socket-name override. */ -void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, - dispatcher_ctx_t *dispatcher, policy_table_t *policy); - -/* Start listening. Returns 0 on success, -1 on error. */ -int server_start(server_ctx_t *ctx); - -/* Get human-readable description of last server error. */ -const char *server_last_error(const server_ctx_t *ctx); - -/* Handle one pending connection (non-blocking). Returns 1 if handled, 0 if nothing pending, -1 on error. - * activity_cb is called with a description string for the TUI activity log. */ -typedef void (*server_activity_cb)(const char *message, void *user_data); -int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data); - -/* Stop server and close socket */ -void server_stop(server_ctx_t *ctx); - -/* Extract caller identity from connected fd */ -int server_get_caller(int fd, caller_identity_t *out); - - -/* from socket_name.h */ - - -/* - * Generate random socket name in format: nsigner__ - * Returns 0 on success, -1 on error. - */ -int socket_name_random(char *out, size_t out_len); - - -/* from main.h */ -/* - * nsigner main header - version information - * - * Version macros are auto-updated by increment_and_push.sh. - */ - - -/* Version information (auto-updated by build/version tooling) */ -#define NSIGNER_VERSION_MAJOR 0 -#define NSIGNER_VERSION_MINOR 0 -#define NSIGNER_VERSION_PATCH 2 -#define NSIGNER_VERSION "v0.0.2" - - -/* NSIGNER_HEADERLESS_DECLS_END */ - -#include -#include - -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++; - } -} - -int main(void) { - char a[128]; - char b[128]; - - memset(a, 0, sizeof(a)); - memset(b, 0, sizeof(b)); - - check_condition("socket_name_random(a) succeeds", socket_name_random(a, sizeof(a)) == 0); - check_condition("socket_name_random(b) succeeds", socket_name_random(b, sizeof(b)) == 0); - - check_condition("name a starts with nsigner_", strncmp(a, "nsigner_", 8) == 0); - check_condition("name b starts with nsigner_", strncmp(b, "nsigner_", 8) == 0); - - { - const char *suffix = a + 8; - const char *underscore = strchr(suffix, '_'); - check_condition("name a contains second underscore", underscore != NULL && underscore > suffix && underscore[1] != '\0'); - } - - { - const char *suffix = b + 8; - const char *underscore = strchr(suffix, '_'); - check_condition("name b contains second underscore", underscore != NULL && underscore > suffix && underscore[1] != '\0'); - } - - check_condition("two generated names are typically different", strcmp(a, b) != 0); - - if (g_failures == 0) { - printf("ALL TESTS PASSED\n"); - return 0; - } - - printf("TESTS FAILED: %d\n", g_failures); - return 1; -}