/* * test_path_whitelist.c — tests for the path-template whitelist and * named path-role functionality. * * Covers: * - server_set_path_whitelist parsing (integer + path-template tokens) * - server_path_whitelist_allows matching * - role_table_register_role_path (idempotent, range fields) * - derive_secp256k1_from_path (BIP-44 path parsing + derivation) */ #define _GNU_SOURCE /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include #include #include #include /* from secure_mem.h */ typedef struct { void *data; size_t size; int locked; } secure_buf_t; int secure_buf_alloc(secure_buf_t *buf, size_t size); void secure_buf_free(secure_buf_t *buf); void secure_memzero(void *ptr, size_t len); /* from mnemonic.h */ #define MNEMONIC_MAX_LEN 256 typedef struct { secure_buf_t buf; int loaded; int word_count; } mnemonic_state_t; void mnemonic_init(mnemonic_state_t *state); int mnemonic_load(mnemonic_state_t *state, const char *phrase); void mnemonic_unload(mnemonic_state_t *state); int mnemonic_is_loaded(const mnemonic_state_t *state); const char *mnemonic_get_phrase(const mnemonic_state_t *state); /* from role_table.h */ #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 #define ROLE_TABLE_MAX_ENTRIES 256 typedef enum { PURPOSE_NOSTR = 0, PURPOSE_BITCOIN, PURPOSE_SSH, PURPOSE_AGE, PURPOSE_FIPS, PURPOSE_PQ_SIG, PURPOSE_PQ_KEM, PURPOSE_UNKNOWN } role_purpose_t; typedef enum { CURVE_SECP256K1 = 0, CURVE_ED25519, CURVE_X25519, CURVE_ML_DSA_65, CURVE_SLH_DSA_128S, CURVE_ML_KEM_768, CURVE_UNKNOWN } role_curve_t; typedef enum { SELECTOR_NOSTR_INDEX, SELECTOR_ROLE_PATH } role_selector_type_t; 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; char role_path[ROLE_PATH_MAX]; char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; int derived; int path_range_lo; int path_range_hi; int path_default_index; int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */ int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */ } role_entry_t; typedef struct { role_entry_t entries[ROLE_TABLE_MAX_ENTRIES]; int count; } role_table_t; void role_table_init(role_table_t *table); int role_table_add(role_table_t *table, const role_entry_t *entry); role_entry_t *role_table_find_by_name(role_table_t *table, const char *name); role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index); role_entry_t *role_table_find_by_path(role_table_t *table, const char *path); role_purpose_t role_purpose_from_str(const char *s); role_curve_t role_curve_from_str(const char *s); const char *role_purpose_to_str(role_purpose_t p); const char *role_curve_to_str(role_curve_t c); int role_table_register_nostr_index(role_table_t *table, int nostr_index); /* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */ int role_table_register_role_path(role_table_t *table, const char *name, const char *path, role_purpose_t purpose, role_curve_t curve, int range_lo, int range_hi, int default_index, const int *allowed_indices, int allowed_count); /* from selector.h */ #define SELECTOR_OK 0 #define SELECTOR_ERR_AMBIGUOUS -1 #define SELECTOR_ERR_NOT_FOUND -2 #define SELECTOR_ERR_NO_DEFAULT -3 typedef struct { int has_role; char role_name[ROLE_NAME_MAX]; int has_nostr_index; int nostr_index; int has_role_path; char role_path[ROLE_PATH_MAX]; int has_index; int index; } selector_request_t; void selector_request_init(selector_request_t *req); int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out); /* from enforcement.h */ #define ENFORCE_OK 0 #define ENFORCE_ERR_PURPOSE -1 #define ENFORCE_ERR_CURVE -2 #define ENFORCE_ERR_UNKNOWN_VERB -3 #define ENFORCE_ERR_ALGORITHM -4 #define VERB_SIGN "sign" #define VERB_VERIFY "verify" #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" #define VERB_DERIVE "derive" #define VERB_GET_PUBLIC_KEY "get_public_key" #define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" #define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" #define VERB_NOSTR_MINE_EVENT "nostr_mine_event" #define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" #define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" #define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" #define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" #define VERB_ENCRYPT "encrypt" #define VERB_DECRYPT "decrypt" int enforce_verb_role(const char *verb, const role_entry_t *role); /* from pq_crypto.h */ typedef enum { CRYPTO_ALG_SECP256K1 = 0, CRYPTO_ALG_ED25519, CRYPTO_ALG_X25519, CRYPTO_ALG_ML_DSA_65, CRYPTO_ALG_SLH_DSA_128S, CRYPTO_ALG_ML_KEM_768, CRYPTO_ALG_UNKNOWN } crypto_alg_t; typedef struct { size_t priv_key_len; size_t pub_key_len; size_t sig_len; size_t ciphertext_len; size_t shared_secret_len; } crypto_alg_sizes_t; const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg); crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose); const char *crypto_alg_to_str(crypto_alg_t alg); crypto_alg_t crypto_alg_from_str(const char *s); /* from key_store.h */ #define KEY_STORE_MAX_ROLES ROLE_TABLE_MAX_ENTRIES typedef struct { secure_buf_t private_key; secure_buf_t public_key; char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */ char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */ crypto_alg_t alg; int valid; } derived_key_t; typedef struct { derived_key_t keys[KEY_STORE_MAX_ROLES]; int count; } key_store_t; int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic); int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index); /* nostr init/cleanup */ int nostr_init(void); void nostr_cleanup(void); /* from server.h (minimal subset for whitelist tests) */ #define SERVER_SOCKET_NAME_MAX 108 #define INDEX_WHITELIST_MAX 256 #define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) #define PATH_WHITELIST_MAX_TEMPLATES 16 #define PATH_TEMPLATE_MAX_LEN 128 typedef struct { char template[PATH_TEMPLATE_MAX_LEN]; int range_lo; int range_hi; int allowed_indices[64]; int allowed_count; } path_template_t; typedef struct { int active; int count; path_template_t templates[PATH_WHITELIST_MAX_TEMPLATES]; } path_whitelist_t; typedef struct { char socket_name[SERVER_SOCKET_NAME_MAX]; char last_error[256]; int listen_fd; int running; int listen_mode; int stdio_handled; void *dispatcher; /* dummy */ void *policy; /* dummy */ int socket_name_explicit; int auth_mode; int auth_skew_seconds; int bridge_source_trusted; int index_whitelist_active; unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; path_whitelist_t path_whitelist; } server_ctx_t; int server_set_index_whitelist(server_ctx_t *ctx, const char *spec); int server_set_path_whitelist(server_ctx_t *ctx, const char *spec); int server_path_whitelist_allows(const server_ctx_t *ctx, const char *role_path); /* NSIGNER_HEADERLESS_DECLS_END */ #include #include #include static int tests_run = 0; static int tests_passed = 0; static void check(const char *desc, int condition) { tests_run++; if (condition) { tests_passed++; printf("PASS: %s\n", desc); } else { printf("FAIL: %s\n", desc); } } int main(void) { server_ctx_t ctx; /* ---- Test 1: server_set_path_whitelist with "all" ---- */ memset(&ctx, 0, sizeof(ctx)); check("set_path_whitelist 'all' returns 0", server_set_path_whitelist(&ctx, "all") == 0); check("'all' sets index_whitelist_active=0", ctx.index_whitelist_active == 0); check("'all' sets path_whitelist.active=0", ctx.path_whitelist.active == 0); /* ---- Test 2: integer-only spec (backward compat) ---- */ memset(&ctx, 0, sizeof(ctx)); check("set_path_whitelist '0-3' returns 0", server_set_path_whitelist(&ctx, "0-3") == 0); check("'0-3' sets index_whitelist_active=1", ctx.index_whitelist_active == 1); check("'0-3' does not set path_whitelist.active", ctx.path_whitelist.active == 0); /* ---- Test 3: path-template spec ---- */ memset(&ctx, 0, sizeof(ctx)); check("set_path_whitelist 'm/44\\'/1237\\'/0-3/1/0' returns 0", server_set_path_whitelist(&ctx, "m/44'/1237'/0-3/1/0") == 0); check("path template sets path_whitelist.active=1", ctx.path_whitelist.active == 1); check("path template count=1", ctx.path_whitelist.count == 1); check("path template range_lo=0", ctx.path_whitelist.templates[0].range_lo == 0); check("path template range_hi=3", ctx.path_whitelist.templates[0].range_hi == 3); /* ---- Test 4: server_path_whitelist_allows matching ---- */ check("path_whitelist_allows m/44'/1237'/1/1/0 (in range)", server_path_whitelist_allows(&ctx, "m/44'/1237'/1/1/0") == 1); check("path_whitelist_allows m/44'/1237'/0/1/0 (in range)", server_path_whitelist_allows(&ctx, "m/44'/1237'/0/1/0") == 1); check("path_whitelist_allows m/44'/1237'/3/1/0 (in range)", server_path_whitelist_allows(&ctx, "m/44'/1237'/3/1/0") == 1); check("path_whitelist denies m/44'/1237'/4/1/0 (out of range)", server_path_whitelist_allows(&ctx, "m/44'/1237'/4/1/0") == 0); check("path_whitelist denies m/44'/1237'/1/0/0 (wrong change)", server_path_whitelist_allows(&ctx, "m/44'/1237'/1/0/0") == 0); /* ---- Test 5: multiple path templates ---- */ memset(&ctx, 0, sizeof(ctx)); check("set_path_whitelist with two templates returns 0", server_set_path_whitelist(&ctx, "m/44'/1237'/0-3/0/0,m/44'/1237'/0-3/1/0") == 0); check("two templates: count=2", ctx.path_whitelist.count == 2); check("two templates: allows m/44'/1237'/2/0/0", server_path_whitelist_allows(&ctx, "m/44'/1237'/2/0/0") == 1); check("two templates: allows m/44'/1237'/2/1/0", server_path_whitelist_allows(&ctx, "m/44'/1237'/2/1/0") == 1); check("two templates: denies m/44'/1237'/2/2/0", server_path_whitelist_allows(&ctx, "m/44'/1237'/2/2/0") == 0); /* ---- Test 6: no path whitelist configured → deny ---- */ memset(&ctx, 0, sizeof(ctx)); check("no path whitelist denies all paths (fail-closed)", server_path_whitelist_allows(&ctx, "m/44'/1237'/1/1/0") == 0); /* ---- Test 7: role_table_register_role_path ---- */ { role_table_t table; role_table_init(&table); check("register_role_path returns 0", role_table_register_role_path(&table, "myrole", "m/44'/1237'/%d/1/0", PURPOSE_NOSTR, CURVE_SECP256K1, 0, 3, 1, NULL, 0) == 0); role_entry_t *r = role_table_find_by_name(&table, "myrole"); check("registered role found by name", r != NULL); check("registered role is SELECTOR_ROLE_PATH", r != NULL && r->selector_type == SELECTOR_ROLE_PATH); check("registered role path_range_lo=0", r != NULL && r->path_range_lo == 0); check("registered role path_range_hi=3", r != NULL && r->path_range_hi == 3); check("registered role path_default_index=1", r != NULL && r->path_default_index == 1); check("registered role purpose=NOSTR", r != NULL && r->purpose == PURPOSE_NOSTR); check("registered role curve=SECP256K1", r != NULL && r->curve == CURVE_SECP256K1); /* Idempotent: registering the same path again returns 0, no duplicate */ check("register_role_path idempotent returns 0", role_table_register_role_path(&table, "other", "m/44'/1237'/%d/1/0", PURPOSE_NOSTR, CURVE_SECP256K1, 0, 3, 1, NULL, 0) == 0); check("idempotent: no duplicate added", table.count == 1); } /* ---- Test 8: end-to-end derivation with role_path ---- */ { role_table_t table; key_store_t key_store; mnemonic_state_t mnemonic; const char *valid_12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; int rc; role_table_init(&table); mnemonic_init(&mnemonic); /* Load mnemonic */ rc = mnemonic_load(&mnemonic, valid_12); check("mnemonic load succeeds", rc == 0); /* Register a fixed-path role (no %d) */ rc = role_table_register_role_path(&table, "testpath", "m/44'/1237'/1/1/0", PURPOSE_NOSTR, CURVE_SECP256K1, 0, 0, -1, NULL, 0); check("register fixed-path role returns 0", rc == 0); /* Derive all keys */ if (nostr_init() != 0) { check("nostr_init succeeds", 0); mnemonic_unload(&mnemonic); printf("\n%d/%d tests passed\n", tests_passed, tests_run); return (tests_passed == tests_run) ? 0 : 1; } check("nostr_init succeeds", 1); memset(&key_store, 0, sizeof(key_store)); rc = crypto_derive_all(&key_store, &table, &mnemonic); check("crypto_derive_all with path role succeeds", rc >= 0); /* Find the role and check it was derived */ role_entry_t *r = role_table_find_by_name(&table, "testpath"); check("testpath role found", r != NULL); check("testpath role derived", r != NULL && r->derived == 1); check("testpath pubkey is 64 hex chars", r != NULL && strlen(r->pubkey_hex) == 64); /* Verify the pubkey matches the expected NIP-06 index-1 derivation * (m/44'/1237'/1'/0/0) — this is a sanity check that the path * derivation produces a valid key. The path m/44'/1237'/1/1/0 is * different from NIP-06 so the pubkey should differ from index 1. */ { role_table_t nip06_table; key_store_t nip06_store; role_table_init(&nip06_table); role_table_register_nostr_index(&nip06_table, 1); memset(&nip06_store, 0, sizeof(nip06_store)); crypto_derive_all(&nip06_store, &nip06_table, &mnemonic); role_entry_t *nip06_r = role_table_find_by_nostr_index(&nip06_table, 1); check("NIP-06 index 1 derived", nip06_r != NULL && nip06_r->derived == 1); check("path m/44'/1237'/1/1/0 differs from NIP-06 index 1 (m/44'/1237'/1'/0/0)", r != NULL && nip06_r != NULL && strcmp(r->pubkey_hex, nip06_r->pubkey_hex) != 0); } mnemonic_unload(&mnemonic); } nostr_cleanup(); printf("\n%d/%d tests passed\n", tests_passed, tests_run); return (tests_passed == tests_run) ? 0 : 1; }