diff --git a/src/main.c b/src/main.c index bc5440e..a9e1d87 100644 --- a/src/main.c +++ b/src/main.c @@ -412,6 +412,9 @@ typedef struct { } caller_identity_t; /* Server context */ +#define INDEX_WHITELIST_MAX 256 +#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) + typedef struct { char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ char last_error[256]; @@ -424,6 +427,9 @@ typedef struct { 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]; } server_ctx_t; /* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). @@ -457,6 +463,9 @@ void server_set_prompt_always_allow(int enabled); /* Mark the unix listener as a trusted bridge socket (qrexec source preamble) */ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled); +/* Set the nostr_index whitelist from a spec string ("all", "1,3,4", "0-3", "0-3,7,9") */ +int server_set_index_whitelist(server_ctx_t *ctx, const char *spec); + /* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */ void server_set_noninteractive_prompt_default(int decision); @@ -494,8 +503,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 37 -#define NSIGNER_VERSION "v0.0.37" +#define NSIGNER_VERSION_PATCH 38 +#define NSIGNER_VERSION "v0.0.38" /* NSIGNER_HEADERLESS_DECLS_END */ @@ -799,6 +808,8 @@ static void print_usage(const char *program_name) { tui_print(" --mnemonic-fd N Read mnemonic from inherited fd N (one line) at startup"); tui_print(" --allow-all, -A Allow all policy prompts for this server session"); tui_print(" --bridge-source-trusted Accept qrexec_source preamble on unix connections (bridge mode)"); + tui_print(" --allow-index SPEC Restrict which nostr_index values this session can access"); + tui_print(" SPEC: 'all' (default), '1,3,4', '0-3', or '0-3,7,9'"); } static int extract_nsigner_socket_from_proc_line(const char *line, @@ -1696,6 +1707,7 @@ int main(int argc, char *argv[]) { int auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS; int allow_all = 0; int bridge_source_trusted = 0; + const char *allow_index_spec = NULL; mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 }; while (argi < argc) { @@ -1806,6 +1818,15 @@ int main(int argc, char *argv[]) { argi += 1; continue; } + if (strcmp(argv[argi], "--allow-index") == 0) { + if (argi + 1 >= argc) { + fprintf(stderr, "Missing value for %s\n", argv[argi]); + return 1; + } + allow_index_spec = argv[argi + 1]; + argi += 2; + continue; + } break; } @@ -2033,6 +2054,16 @@ int main(int argc, char *argv[]) { if (bridge_source_trusted) { server_set_bridge_source_trusted(&server, 1); } + if (allow_index_spec != NULL) { + if (server_set_index_whitelist(&server, allow_index_spec) != 0) { + fprintf(stderr, "Invalid --allow-index spec: %s\n", allow_index_spec); + fprintf(stderr, "Expected: 'all', '1,3,4', '0-3', or '0-3,7,9'\n"); + crypto_wipe(&key_store); + nostr_cleanup(); + mnemonic_unload(&mnemonic); + return 1; + } + } if (server_start(&server) != 0) { if (listen_mode == NSIGNER_LISTEN_UNIX) { fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server)); @@ -2065,6 +2096,9 @@ int main(int argc, char *argv[]) { auth_skew_seconds, &dispatcher, &policy); + if (allow_index_spec != NULL) { + server_set_index_whitelist(&servers[tcp_server_idx], allow_index_spec); + } if (server_start(&servers[tcp_server_idx]) != 0) { fprintf(stderr, "Failed to start TCP server on %s: %s\n", tcp_target, server_last_error(&servers[tcp_server_idx])); diff --git a/src/server.c b/src/server.c index cf41a31..9b72e09 100644 --- a/src/server.c +++ b/src/server.c @@ -415,6 +415,9 @@ typedef struct { } caller_identity_t; /* Server context */ +#define INDEX_WHITELIST_MAX 256 /* nostr_index range 0-255 */ +#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) /* 32 bytes */ + typedef struct { char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ char last_error[256]; @@ -428,6 +431,8 @@ typedef struct { int auth_mode; int auth_skew_seconds; int bridge_source_trusted; /* when set, unix connections send a qrexec_source preamble */ + int index_whitelist_active; /* 1 if index whitelist is set (not "all") */ + unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; /* bitmap of allowed nostr_index values */ } server_ctx_t; /* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). @@ -444,6 +449,15 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp * identity is composed as qubes:. */ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled); +/* Set the nostr_index whitelist from a parsed spec string. + * Spec format: "all" (no restriction), or comma-separated list of + * indices and ranges, e.g. "1,3,4" or "0-3" or "0-3,7,9". + * Returns 0 on success, -1 on parse error. */ +int server_set_index_whitelist(server_ctx_t *ctx, const char *spec); + +/* Check if a nostr_index is in the whitelist. Returns 1 if allowed, 0 if not. */ +int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index); + /* Start listening. Returns 0 on success, -1 on error. */ int server_start(server_ctx_t *ctx); @@ -1058,6 +1072,8 @@ void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_exp ctx->auth_mode = auth_mode; ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS; ctx->bridge_source_trusted = 0; + ctx->index_whitelist_active = 0; + memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist)); if (!g_auth_nonce_cache_inited) { auth_nonce_cache_init(&g_auth_nonce_cache); g_auth_nonce_cache_inited = 1; @@ -1071,6 +1087,87 @@ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled) { ctx->bridge_source_trusted = enabled ? 1 : 0; } +static void whitelist_set_bit(unsigned char *bitmap, int index) { + if (index >= 0 && index < INDEX_WHITELIST_MAX) { + bitmap[index / 8] |= (1 << (index % 8)); + } +} + +static int whitelist_get_bit(const unsigned char *bitmap, int index) { + if (index < 0 || index >= INDEX_WHITELIST_MAX) { + return 0; + } + return (bitmap[index / 8] >> (index % 8)) & 1; +} + +int server_set_index_whitelist(server_ctx_t *ctx, const char *spec) { + char buf[256]; + char *p; + char *token; + + if (ctx == NULL || spec == NULL) { + return -1; + } + + /* "all" means no restriction */ + if (strcmp(spec, "all") == 0) { + ctx->index_whitelist_active = 0; + memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist)); + return 0; + } + + strncpy(buf, spec, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + /* Parse comma-separated entries: "1,3,4" or "0-3" or "0-3,7,9" */ + memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist)); + p = buf; + while (p != NULL && *p != '\0') { + char *comma = strchr(p, ','); + if (comma != NULL) { + *comma = '\0'; + } + + /* Parse token: either "N" or "N-M" (range) */ + char *dash = strchr(p, '-'); + if (dash != NULL) { + *dash = '\0'; + char *endptr1 = NULL, *endptr2 = NULL; + long lo = strtol(p, &endptr1, 10); + long hi = strtol(dash + 1, &endptr2, 10); + if (*endptr1 != '\0' || *endptr2 != '\0' || lo < 0 || hi < 0 || + lo >= INDEX_WHITELIST_MAX || hi >= INDEX_WHITELIST_MAX || lo > hi) { + return -1; + } + for (long i = lo; i <= hi; i++) { + whitelist_set_bit(ctx->index_whitelist, (int)i); + } + } else { + char *endptr = NULL; + long idx = strtol(p, &endptr, 10); + if (*endptr != '\0' || idx < 0 || idx >= INDEX_WHITELIST_MAX) { + return -1; + } + whitelist_set_bit(ctx->index_whitelist, (int)idx); + } + + p = (comma != NULL) ? comma + 1 : NULL; + } + + ctx->index_whitelist_active = 1; + return 0; +} + +int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index) { + if (ctx == NULL) { + return 1; /* no ctx = allow (shouldn't happen) */ + } + if (!ctx->index_whitelist_active) { + return 1; /* "all" mode = no restriction */ + } + return whitelist_get_bit(ctx->index_whitelist, nostr_index); +} + int server_start(server_ctx_t *ctx) { int fd; struct sockaddr_un addr; @@ -1569,6 +1666,24 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) { } } + /* + * Index whitelist enforcement: if the request specifies a nostr_index + * and the whitelist is active (not "all"), check that the index is + * allowed. This is a hard deny — it happens before policy/approval, + * so even an approved caller cannot access a non-whitelisted index. + * We set hard_selector_error to a sentinel so the policy_check branch + * is skipped. + */ + if (hard_selector_error == 0 && selector_req.has_nostr_index && + !server_index_whitelist_allows(ctx, selector_req.nostr_index)) { + (void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index); + response = strdup("{\"id\":\"null\",\"error\":{\"code\":2002,\"message\":\"index_not_allowed\"}}"); + pchk = POLICY_DENY; + verdict = "DENIED"; + source_label = "index-whitelist"; + hard_selector_error = -100; /* sentinel: skip policy_check */ + } + if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}"); pchk = POLICY_DENY; @@ -1578,9 +1693,11 @@ int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) { } else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}"); pchk = POLICY_DENY; - } else { + } else if (hard_selector_error == 0) { + /* Normal path: run policy_check (skip if whitelist already denied) */ pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src); } + /* else: hard_selector_error == -100 (whitelist deny) — keep pchk=POLICY_DENY */ if (pchk == POLICY_PROMPT) { pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);