v0.0.38 - Add --allow-index whitelist for nostr_index restriction — supports list (1,3,4), range (0-3), mixed (0,2-3), or 'all'
This commit is contained in:
38
src/main.c
38
src/main.c
@@ -412,6 +412,9 @@ typedef struct {
|
|||||||
} caller_identity_t;
|
} caller_identity_t;
|
||||||
|
|
||||||
/* Server context */
|
/* Server context */
|
||||||
|
#define INDEX_WHITELIST_MAX 256
|
||||||
|
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8)
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
|
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
|
||||||
char last_error[256];
|
char last_error[256];
|
||||||
@@ -424,6 +427,9 @@ typedef struct {
|
|||||||
int socket_name_explicit;
|
int socket_name_explicit;
|
||||||
int auth_mode;
|
int auth_mode;
|
||||||
int auth_skew_seconds;
|
int auth_skew_seconds;
|
||||||
|
int bridge_source_trusted;
|
||||||
|
int index_whitelist_active;
|
||||||
|
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE];
|
||||||
} server_ctx_t;
|
} server_ctx_t;
|
||||||
|
|
||||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
/* 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) */
|
/* Mark the unix listener as a trusted bridge socket (qrexec source preamble) */
|
||||||
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
|
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 */
|
/* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */
|
||||||
void server_set_noninteractive_prompt_default(int decision);
|
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) */
|
/* Version information (auto-updated by build/version tooling) */
|
||||||
#define NSIGNER_VERSION_MAJOR 0
|
#define NSIGNER_VERSION_MAJOR 0
|
||||||
#define NSIGNER_VERSION_MINOR 0
|
#define NSIGNER_VERSION_MINOR 0
|
||||||
#define NSIGNER_VERSION_PATCH 37
|
#define NSIGNER_VERSION_PATCH 38
|
||||||
#define NSIGNER_VERSION "v0.0.37"
|
#define NSIGNER_VERSION "v0.0.38"
|
||||||
|
|
||||||
|
|
||||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
/* 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(" --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(" --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(" --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,
|
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 auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS;
|
||||||
int allow_all = 0;
|
int allow_all = 0;
|
||||||
int bridge_source_trusted = 0;
|
int bridge_source_trusted = 0;
|
||||||
|
const char *allow_index_spec = NULL;
|
||||||
mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 };
|
mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 };
|
||||||
|
|
||||||
while (argi < argc) {
|
while (argi < argc) {
|
||||||
@@ -1806,6 +1818,15 @@ int main(int argc, char *argv[]) {
|
|||||||
argi += 1;
|
argi += 1;
|
||||||
continue;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2033,6 +2054,16 @@ int main(int argc, char *argv[]) {
|
|||||||
if (bridge_source_trusted) {
|
if (bridge_source_trusted) {
|
||||||
server_set_bridge_source_trusted(&server, 1);
|
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 (server_start(&server) != 0) {
|
||||||
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
if (listen_mode == NSIGNER_LISTEN_UNIX) {
|
||||||
fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server));
|
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,
|
auth_skew_seconds,
|
||||||
&dispatcher,
|
&dispatcher,
|
||||||
&policy);
|
&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) {
|
if (server_start(&servers[tcp_server_idx]) != 0) {
|
||||||
fprintf(stderr, "Failed to start TCP server on %s: %s\n",
|
fprintf(stderr, "Failed to start TCP server on %s: %s\n",
|
||||||
tcp_target, server_last_error(&servers[tcp_server_idx]));
|
tcp_target, server_last_error(&servers[tcp_server_idx]));
|
||||||
|
|||||||
119
src/server.c
119
src/server.c
@@ -415,6 +415,9 @@ typedef struct {
|
|||||||
} caller_identity_t;
|
} caller_identity_t;
|
||||||
|
|
||||||
/* Server context */
|
/* 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 {
|
typedef struct {
|
||||||
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
|
char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */
|
||||||
char last_error[256];
|
char last_error[256];
|
||||||
@@ -428,6 +431,8 @@ typedef struct {
|
|||||||
int auth_mode;
|
int auth_mode;
|
||||||
int auth_skew_seconds;
|
int auth_skew_seconds;
|
||||||
int bridge_source_trusted; /* when set, unix connections send a qrexec_source preamble */
|
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;
|
} server_ctx_t;
|
||||||
|
|
||||||
/* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner").
|
/* 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:<vm>. */
|
* identity is composed as qubes:<vm>. */
|
||||||
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
|
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. */
|
/* Start listening. Returns 0 on success, -1 on error. */
|
||||||
int server_start(server_ctx_t *ctx);
|
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_mode = auth_mode;
|
||||||
ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS;
|
ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS;
|
||||||
ctx->bridge_source_trusted = 0;
|
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) {
|
if (!g_auth_nonce_cache_inited) {
|
||||||
auth_nonce_cache_init(&g_auth_nonce_cache);
|
auth_nonce_cache_init(&g_auth_nonce_cache);
|
||||||
g_auth_nonce_cache_inited = 1;
|
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;
|
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 server_start(server_ctx_t *ctx) {
|
||||||
int fd;
|
int fd;
|
||||||
struct sockaddr_un addr;
|
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) {
|
if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) {
|
||||||
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}");
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}");
|
||||||
pchk = POLICY_DENY;
|
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) {
|
} else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) {
|
||||||
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
|
||||||
pchk = POLICY_DENY;
|
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);
|
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) {
|
if (pchk == POLICY_PROMPT) {
|
||||||
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
|
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
|
||||||
|
|||||||
Reference in New Issue
Block a user