444 lines
16 KiB
C
444 lines
16 KiB
C
/*
|
|
* 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 <stddef.h>
|
|
#include <stdint.h>
|
|
#include <sys/types.h>
|
|
#include <cJSON.h>
|
|
|
|
/* 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 <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
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;
|
|
}
|