#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 256 /* 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); /* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */ int role_table_register_nostr_index(role_table_t *table, int nostr_index); /* 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 160 /* Prompt behavior */ typedef enum { PROMPT_NEVER = 0, PROMPT_FIRST_PER_BOOT, PROMPT_EVERY_REQUEST, PROMPT_DENY } prompt_mode_t; typedef enum { POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */ POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */ POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */ } policy_source_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_source_t source; } 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_ALLOW_SESSION_VERB 3 /* allow + save session grant for this caller+role+verb */ #define POLICY_ALLOW_SESSION_ALL 4 /* allow + save session grant for this caller+role (all verbs) */ #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); /* Insert an entry before the final catch-all rule. */ int policy_table_insert_before_last(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, policy_source_t *out_source); /* 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); /* Derive key for exactly one role index. Returns 0 on success, -1 on error. */ int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index); /* 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 #define NSIGNER_LISTEN_UNIX 0 #define NSIGNER_LISTEN_STDIO 1 #define NSIGNER_LISTEN_QREXEC 2 #define NSIGNER_LISTEN_TCP 3 #define NSIGNER_AUTH_OFF 0 #define NSIGNER_AUTH_OPTIONAL 1 #define NSIGNER_AUTH_REQUIRED 2 /* Caller identity */ typedef struct { uid_t uid; gid_t gid; pid_t pid; int kind; char caller_id[POLICY_CALLER_MAX_LEN]; /* "uid:", "qubes:", or "pubkey:" */ char source_qube[64]; int auth_present; char auth_pubkey_hex[65]; char auth_label[64]; } 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; int listen_mode; int stdio_handled; dispatcher_ctx_t *dispatcher; policy_table_t *policy; int socket_name_explicit; int auth_mode; int auth_skew_seconds; } 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, int listen_mode, int auth_mode, int auth_skew_seconds, 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 */ int transport_send_framed(int fd, const char *payload); int transport_recv_framed(int fd, char **out_payload, size_t max_size); #include "auth_envelope.h" #include #include #include #include #include #include #include #include #include #include static int g_prompt_always_allow = 0; static int g_noninteractive_prompt_default = -1; static auth_nonce_cache_t g_auth_nonce_cache; static int g_auth_nonce_cache_inited = 0; static int caller_id_extract_ipv6(const char *caller_id, char *out_ipv6, size_t out_sz) { const char *start; const char *end; size_t len; if (caller_id == NULL || out_ipv6 == NULL || out_sz == 0) { return -1; } if (strncmp(caller_id, "tcp:[", 5) != 0) { return -1; } start = caller_id + 5; end = strchr(start, ']'); if (end == NULL || end[1] != ':') { return -1; } len = (size_t)(end - start); if (len == 0 || len >= out_sz) { return -1; } memcpy(out_ipv6, start, len); out_ipv6[len] = '\0'; return 0; } static int read_cmd_output(const char *cmd, char **out_buf) { FILE *fp; char chunk[512]; char *buf = NULL; size_t used = 0; size_t cap = 0; if (cmd == NULL || out_buf == NULL) { return -1; } *out_buf = NULL; fp = popen(cmd, "r"); if (fp == NULL) { return -1; } while (fgets(chunk, sizeof(chunk), fp) != NULL) { size_t n = strlen(chunk); if (used + n + 1 > cap) { size_t new_cap = (cap == 0) ? 2048 : cap * 2; while (new_cap < used + n + 1) { new_cap *= 2; } { char *tmp = (char *)realloc(buf, new_cap); if (tmp == NULL) { free(buf); (void)pclose(fp); return -1; } buf = tmp; cap = new_cap; } } memcpy(buf + used, chunk, n); used += n; } (void)pclose(fp); if (buf == NULL) { return -1; } buf[used] = '\0'; *out_buf = buf; return 0; } static int lookup_fips_peer_for_ipv6(const char *ipv6, char *out_npub, size_t out_npub_sz, char *out_name, size_t out_name_sz) { char *json = NULL; cJSON *root = NULL; cJSON *peers = NULL; int i; if (ipv6 == NULL || out_npub == NULL || out_npub_sz == 0 || out_name == NULL || out_name_sz == 0) { return -1; } out_npub[0] = '\0'; out_name[0] = '\0'; if (read_cmd_output("fipsctl show peers 2>/dev/null", &json) != 0) { return -1; } root = cJSON_Parse(json); free(json); if (root == NULL) { return -1; } peers = cJSON_GetObjectItemCaseSensitive(root, "peers"); if (!cJSON_IsArray(peers)) { cJSON_Delete(root); return -1; } for (i = 0; i < cJSON_GetArraySize(peers); ++i) { cJSON *peer = cJSON_GetArrayItem(peers, i); cJSON *peer_ip; cJSON *peer_npub; cJSON *peer_name; if (!cJSON_IsObject(peer)) { continue; } peer_ip = cJSON_GetObjectItemCaseSensitive(peer, "ipv6_addr"); if (!cJSON_IsString(peer_ip) || peer_ip->valuestring == NULL) { continue; } if (strcmp(peer_ip->valuestring, ipv6) != 0) { continue; } peer_npub = cJSON_GetObjectItemCaseSensitive(peer, "npub"); peer_name = cJSON_GetObjectItemCaseSensitive(peer, "display_name"); if (cJSON_IsString(peer_npub) && peer_npub->valuestring != NULL) { strncpy(out_npub, peer_npub->valuestring, out_npub_sz - 1); out_npub[out_npub_sz - 1] = '\0'; } if (cJSON_IsString(peer_name) && peer_name->valuestring != NULL) { strncpy(out_name, peer_name->valuestring, out_name_sz - 1); out_name[out_name_sz - 1] = '\0'; } cJSON_Delete(root); return (out_npub[0] != '\0') ? 0 : -1; } cJSON_Delete(root); return -1; } void server_set_prompt_always_allow(int enabled) { g_prompt_always_allow = enabled ? 1 : 0; } void server_set_noninteractive_prompt_default(int decision) { if (decision == POLICY_ALLOW || decision == POLICY_DENY) { g_noninteractive_prompt_default = decision; } else { g_noninteractive_prompt_default = -1; } } static int prompt_for_policy_decision(const caller_identity_t *caller, const char *method, const char *role_name, const char *purpose, int pending_derivation) { int ch; if (g_prompt_always_allow) { return POLICY_ALLOW; } if (!isatty(STDIN_FILENO)) { if (g_noninteractive_prompt_default == POLICY_ALLOW || g_noninteractive_prompt_default == POLICY_DENY) { return g_noninteractive_prompt_default; } return POLICY_DENY; } printf("\nApproval required\n"); printf("caller: %s\n", (caller != NULL) ? caller->caller_id : "unknown"); if (caller != NULL && caller->kind == NSIGNER_LISTEN_TCP) { char ipv6[INET6_ADDRSTRLEN]; char npub[128]; char display_name[128]; if (caller_id_extract_ipv6(caller->caller_id, ipv6, sizeof(ipv6)) == 0 && lookup_fips_peer_for_ipv6(ipv6, npub, sizeof(npub), display_name, sizeof(display_name)) == 0) { if (display_name[0] != '\0') { printf("fips peer: %s (%s)\n", npub, display_name); } else { printf("fips peer: %s\n", npub); } } } printf("method: %s\n", (method != NULL) ? method : "unknown"); printf("role: %s\n", (role_name != NULL) ? role_name : "unknown"); printf("purpose: %s\n", (purpose != NULL) ? purpose : "unknown"); if (pending_derivation) { printf(" ** NEW IDENTITY — will be derived if approved **\n"); } printf("[y] allow once [n] deny [e] allow this caller+role+verb for session\n"); printf("[a] allow this caller+role for session (all verbs)\n> "); fflush(stdout); ch = getchar(); while (ch != EOF && ch != '\n') { int next = getchar(); if (next == EOF || next == '\n') { break; } } ch = tolower(ch); if (ch == 'a') { return POLICY_ALLOW_SESSION_ALL; } if (ch == 'e') { return POLICY_ALLOW_SESSION_VERB; } if (ch == 'y') { return POLICY_ALLOW; } return POLICY_DENY; } static void server_set_error(server_ctx_t *ctx, const char *msg) { if (ctx == NULL) { return; } if (msg == NULL) { ctx->last_error[0] = '\0'; return; } strncpy(ctx->last_error, msg, sizeof(ctx->last_error) - 1); ctx->last_error[sizeof(ctx->last_error) - 1] = '\0'; } static int parse_tcp_target(const char *target, int *out_family, char *out_host, size_t out_host_sz, uint16_t *out_port) { const char *p; const char *host_start; const char *host_end; const char *port_start; char port_buf[16]; size_t host_len; size_t port_len; char *endptr = NULL; long port_long; if (target == NULL || out_family == NULL || out_host == NULL || out_port == NULL || out_host_sz == 0) { return -1; } if (strncmp(target, "tcp:", 4) != 0) { return -1; } p = target + 4; if (*p == '[') { host_start = p + 1; host_end = strchr(host_start, ']'); if (host_end == NULL || host_end[1] != ':') { return -1; } port_start = host_end + 2; } else { host_start = p; host_end = strrchr(p, ':'); if (host_end == NULL || host_end == host_start) { return -1; } port_start = host_end + 1; } host_len = (size_t)(host_end - host_start); if (host_len == 0 || host_len >= out_host_sz) { return -1; } memcpy(out_host, host_start, host_len); out_host[host_len] = '\0'; port_len = strlen(port_start); if (port_len == 0 || port_len >= sizeof(port_buf)) { return -1; } memcpy(port_buf, port_start, port_len + 1); errno = 0; port_long = strtol(port_buf, &endptr, 10); if (errno != 0 || endptr == port_buf || *endptr != '\0' || port_long < 1 || port_long > 65535) { return -1; } { struct in6_addr addr6; struct in_addr addr4; if (inet_pton(AF_INET6, out_host, &addr6) == 1) { *out_family = AF_INET6; } else if (inet_pton(AF_INET, out_host, &addr4) == 1) { *out_family = AF_INET; } else { return -1; } } *out_port = (uint16_t)port_long; return 0; } static void json_copy_string(char *dst, size_t dst_sz, const char *src, const char *fallback) { const char *s = (src != NULL) ? src : fallback; if (dst == NULL || dst_sz == 0) { return; } if (s == NULL) { dst[0] = '\0'; return; } strncpy(dst, s, dst_sz - 1); dst[dst_sz - 1] = '\0'; } static int extract_request_id_compact(const char *json, char *out_id, size_t out_id_sz) { cJSON *root; cJSON *id_item; char *printed = NULL; if (json == NULL || out_id == NULL || out_id_sz == 0) { return -1; } out_id[0] = '\0'; root = cJSON_Parse(json); if (root == NULL) { return -1; } id_item = cJSON_GetObjectItemCaseSensitive(root, "id"); if (id_item == NULL) { cJSON_Delete(root); return -1; } if (cJSON_IsString(id_item) && id_item->valuestring != NULL) { json_copy_string(out_id, out_id_sz, id_item->valuestring, "null"); cJSON_Delete(root); return 0; } printed = cJSON_PrintUnformatted(id_item); if (printed != NULL) { json_copy_string(out_id, out_id_sz, printed, "null"); free(printed); cJSON_Delete(root); return 0; } cJSON_Delete(root); return -1; } static int request_has_auth_field(const char *json) { cJSON *root; cJSON *auth; int has_auth; if (json == NULL) { return 0; } root = cJSON_Parse(json); if (root == NULL) { return 0; } auth = cJSON_GetObjectItemCaseSensitive(root, "auth"); has_auth = (auth != NULL) ? 1 : 0; cJSON_Delete(root); return has_auth; } static void caller_id_compose_with_pubkey(char *out, size_t out_sz, int caller_kind, const char *source_qube, const char *pubkey_hex) { if (out == NULL || out_sz == 0 || pubkey_hex == NULL) { return; } if (caller_kind == NSIGNER_LISTEN_QREXEC && source_qube != NULL && source_qube[0] != '\0') { (void)snprintf(out, out_sz, "qubes:%s+pubkey:%s", source_qube, pubkey_hex); return; } (void)snprintf(out, out_sz, "pubkey:%s", pubkey_hex); } static int extract_method_and_selector(const char *json, char *method, size_t method_sz, selector_request_t *selector_req) { cJSON *root; cJSON *method_item; cJSON *params_item; cJSON *options_item; cJSON *tmp; if (json == NULL || method == NULL || selector_req == NULL) { return -1; } method[0] = '\0'; selector_request_init(selector_req); root = cJSON_Parse(json); if (root == NULL) { return -1; } method_item = cJSON_GetObjectItemCaseSensitive(root, "method"); if (!cJSON_IsString(method_item) || method_item->valuestring == NULL) { cJSON_Delete(root); return -1; } json_copy_string(method, method_sz, method_item->valuestring, "unknown"); params_item = cJSON_GetObjectItemCaseSensitive(root, "params"); if (cJSON_IsArray(params_item)) { { int params_count = cJSON_GetArraySize(params_item); options_item = (params_count > 0) ? cJSON_GetArrayItem(params_item, params_count - 1) : NULL; } if (cJSON_IsObject(options_item)) { tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role"); if (cJSON_IsString(tmp) && tmp->valuestring != NULL) { selector_req->has_role = 1; json_copy_string(selector_req->role_name, sizeof(selector_req->role_name), tmp->valuestring, ""); } tmp = cJSON_GetObjectItemCaseSensitive(options_item, "nostr_index"); if (cJSON_IsNumber(tmp)) { selector_req->has_nostr_index = 1; selector_req->nostr_index = tmp->valueint; } tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role_path"); if (cJSON_IsString(tmp) && tmp->valuestring != NULL) { selector_req->has_role_path = 1; json_copy_string(selector_req->role_path, sizeof(selector_req->role_path), tmp->valuestring, ""); } } } cJSON_Delete(root); return 0; } void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit, int listen_mode, int auth_mode, int auth_skew_seconds, dispatcher_ctx_t *dispatcher, policy_table_t *policy) { if (ctx == NULL) { return; } memset(ctx, 0, sizeof(*ctx)); ctx->last_error[0] = '\0'; if (socket_name != NULL) { strncpy(ctx->socket_name, socket_name, sizeof(ctx->socket_name) - 1); ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0'; } ctx->listen_fd = -1; ctx->listen_mode = listen_mode; ctx->stdio_handled = 0; ctx->dispatcher = dispatcher; ctx->policy = policy; ctx->socket_name_explicit = socket_name_explicit ? 1 : 0; ctx->auth_mode = auth_mode; ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS; if (!g_auth_nonce_cache_inited) { auth_nonce_cache_init(&g_auth_nonce_cache); g_auth_nonce_cache_inited = 1; } } int server_start(server_ctx_t *ctx) { int fd; struct sockaddr_un addr; socklen_t addr_len; int flags; int attempt; if (ctx == NULL) { return -1; } if (ctx->dispatcher == NULL || ctx->policy == NULL || ctx->socket_name[0] == '\0') { server_set_error(ctx, "invalid server context (missing dispatcher/policy/socket_name)"); return -1; } server_set_error(ctx, NULL); if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) { ctx->listen_fd = STDIN_FILENO; ctx->running = 1; ctx->stdio_handled = 0; server_set_error(ctx, NULL); return 0; } if (ctx->listen_mode == NSIGNER_LISTEN_TCP) { int family; uint16_t port; char host[64]; int one = 1; int prc = parse_tcp_target(ctx->socket_name, &family, host, sizeof(host), &port); if (prc != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "invalid tcp listen target: %s (expected tcp:IPv4:PORT or tcp:[IPv6]:PORT)", ctx->socket_name); return -1; } fd = socket(family, SOCK_STREAM, 0); if (fd < 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "socket(tcp) failed: %s", strerror(errno)); return -1; } (void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); if (family == AF_INET) { struct sockaddr_in addr4; memset(&addr4, 0, sizeof(addr4)); addr4.sin_family = AF_INET; addr4.sin_port = htons(port); if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) { close(fd); server_set_error(ctx, "inet_pton(AF_INET) failed for listen target"); return -1; } if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "bind(%s) failed: %s", ctx->socket_name, strerror(errno)); close(fd); return -1; } } else { struct sockaddr_in6 addr6; memset(&addr6, 0, sizeof(addr6)); addr6.sin6_family = AF_INET6; addr6.sin6_port = htons(port); if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) { close(fd); server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target"); return -1; } if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "bind(%s) failed: %s", ctx->socket_name, strerror(errno)); close(fd); return -1; } } if (listen(fd, 16) != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "listen() failed: %s", strerror(errno)); close(fd); return -1; } flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "fcntl(O_NONBLOCK) failed: %s", strerror(errno)); close(fd); return -1; } ctx->listen_fd = fd; ctx->running = 1; server_set_error(ctx, NULL); return 0; } fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "socket(AF_UNIX, SOCK_STREAM) failed: %s", strerror(errno)); return -1; } for (attempt = 0; attempt < 8; ++attempt) { memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; addr.sun_path[0] = '\0'; strncpy(&addr.sun_path[1], ctx->socket_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(ctx->socket_name)); if (bind(fd, (struct sockaddr *)&addr, addr_len) == 0) { break; } if (errno == EADDRINUSE && !ctx->socket_name_explicit) { if (socket_name_random(ctx->socket_name, sizeof(ctx->socket_name)) == 0) { continue; } (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "bind(@%s) failed: %s (and failed to generate retry socket name)", ctx->socket_name, strerror(errno)); close(fd); return -1; } if (errno == EADDRINUSE && ctx->socket_name_explicit) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "bind(@%s) failed: %s (explicit --socket-name is already in use)", ctx->socket_name, strerror(errno)); } else { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "bind(@%s) failed: %s", ctx->socket_name, strerror(errno)); } close(fd); return -1; } if (attempt >= 8) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "bind failed after retries: address already in use for generated names (try --socket-name)"); close(fd); return -1; } if (listen(fd, 5) != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "listen() failed: %s", strerror(errno)); close(fd); return -1; } flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { (void)snprintf(ctx->last_error, sizeof(ctx->last_error), "fcntl(O_NONBLOCK) failed: %s", strerror(errno)); close(fd); return -1; } ctx->listen_fd = fd; ctx->running = 1; server_set_error(ctx, NULL); return 0; } const char *server_last_error(const server_ctx_t *ctx) { if (ctx == NULL || ctx->last_error[0] == '\0') { return "unknown server error"; } return ctx->last_error; } int server_get_caller(int fd, caller_identity_t *out) { struct ucred cred; socklen_t len = sizeof(cred); if (out == NULL) { return -1; } memset(out, 0, sizeof(*out)); if (fd == STDIN_FILENO) { const char *src = getenv("QREXEC_REMOTE_DOMAIN"); out->kind = NSIGNER_LISTEN_STDIO; if (src != NULL && src[0] != '\0') { out->kind = NSIGNER_LISTEN_QREXEC; strncpy(out->source_qube, src, sizeof(out->source_qube) - 1); out->source_qube[sizeof(out->source_qube) - 1] = '\0'; (void)snprintf(out->caller_id, sizeof(out->caller_id), "qubes:%.57s", out->source_qube); return 0; } out->uid = getuid(); out->gid = getgid(); out->pid = getpid(); (void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid); return 0; } { struct sockaddr_storage peer; socklen_t peer_len = sizeof(peer); if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) == 0) { if (peer.ss_family == AF_INET) { const struct sockaddr_in *in4 = (const struct sockaddr_in *)&peer; char ip[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &in4->sin_addr, ip, sizeof(ip)) != NULL) { out->kind = NSIGNER_LISTEN_TCP; (void)snprintf(out->caller_id, sizeof(out->caller_id), "tcp:%s:%u", ip, (unsigned int)ntohs(in4->sin_port)); return 0; } } else if (peer.ss_family == AF_INET6) { const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&peer; char ip6[INET6_ADDRSTRLEN]; if (inet_ntop(AF_INET6, &in6->sin6_addr, ip6, sizeof(ip6)) != NULL) { out->kind = NSIGNER_LISTEN_TCP; (void)snprintf(out->caller_id, sizeof(out->caller_id), "tcp:[%s]:%u", ip6, (unsigned int)ntohs(in6->sin6_port)); return 0; } } } } if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) { return -1; } out->uid = cred.uid; out->gid = cred.gid; out->pid = cred.pid; out->kind = NSIGNER_LISTEN_UNIX; (void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid); return 0; } int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) { int client_fd; caller_identity_t caller; char *request = NULL; char *response = NULL; char method[64]; char request_id[128]; char auth_pubkey[65]; char auth_label[64]; char role_name[ROLE_NAME_MAX]; char purpose[ROLE_PURPOSE_MAX]; selector_request_t selector_req; role_entry_t *role = NULL; int selector_rc; int pchk; policy_source_t policy_src = POLICY_SOURCE_DEFAULT; int pending_derivation = 0; int hard_selector_error = 0; int derivation_error = 0; char activity[256]; const char *verdict = "DENIED"; const char *source_label = "no-match"; int auth_err_code = 0; const char *auth_err_msg = NULL; if (ctx == NULL || ctx->listen_fd < 0 || ctx->dispatcher == NULL || ctx->policy == NULL) { return -1; } if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) { if (ctx->stdio_handled) { return 0; } client_fd = STDIN_FILENO; ctx->stdio_handled = 1; } else { client_fd = accept(ctx->listen_fd, NULL, NULL); if (client_fd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; } return -1; } } if (server_get_caller(client_fd, &caller) != 0) { if (client_fd != STDIN_FILENO) { close(client_fd); } return -1; } if (transport_recv_framed(client_fd, &request, SERVER_MAX_MSG_SIZE) != 0) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}"); if (response != NULL) { (void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response); free(response); } if (client_fd != STDIN_FILENO) { close(client_fd); } return 1; } json_copy_string(method, sizeof(method), "unknown", "unknown"); json_copy_string(role_name, sizeof(role_name), "unknown", "unknown"); json_copy_string(purpose, sizeof(purpose), "unknown", "unknown"); json_copy_string(request_id, sizeof(request_id), "null", "null"); auth_pubkey[0] = '\0'; auth_label[0] = '\0'; { int auth_required = 0; int auth_optional = 0; int has_auth_field = 0; if (caller.kind == NSIGNER_LISTEN_TCP) { auth_required = 1; } else if (caller.kind == NSIGNER_LISTEN_QREXEC) { if (ctx->auth_mode == NSIGNER_AUTH_REQUIRED) { auth_required = 1; } else if (ctx->auth_mode == NSIGNER_AUTH_OPTIONAL) { auth_optional = 1; } } has_auth_field = request_has_auth_field(request); if (auth_required || (auth_optional && has_auth_field)) { if (auth_envelope_verify_request(request, &g_auth_nonce_cache, ctx->auth_skew_seconds, auth_pubkey, sizeof(auth_pubkey), auth_label, sizeof(auth_label), &auth_err_code, &auth_err_msg) != 0) { (void)extract_request_id_compact(request, request_id, sizeof(request_id)); if (auth_err_msg == NULL) { auth_err_msg = "auth_envelope_malformed"; } { char errbuf[256]; (void)snprintf(errbuf, sizeof(errbuf), "{\"id\":\"%s\",\"error\":{\"code\":%d,\"message\":\"%s\"}}", request_id, (auth_err_code != 0) ? auth_err_code : AUTH_ERR_ENVELOPE_MALFORMED, auth_err_msg); response = strdup(errbuf); } if (response != NULL) { (void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response); free(response); } free(request); if (client_fd != STDIN_FILENO) { close(client_fd); } return 1; } caller.auth_present = 1; json_copy_string(caller.auth_pubkey_hex, sizeof(caller.auth_pubkey_hex), auth_pubkey, ""); json_copy_string(caller.auth_label, sizeof(caller.auth_label), auth_label, ""); caller_id_compose_with_pubkey(caller.caller_id, sizeof(caller.caller_id), caller.kind, caller.source_qube, auth_pubkey); } } if (extract_method_and_selector(request, method, sizeof(method), &selector_req) == 0) { if (ctx->dispatcher->role_table != NULL) { selector_rc = selector_resolve(&selector_req, ctx->dispatcher->role_table, &role); if (selector_rc == SELECTOR_OK && role != NULL) { json_copy_string(role_name, sizeof(role_name), role->name, "main"); json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr"); } else if (selector_rc == SELECTOR_ERR_NOT_FOUND && selector_req.has_nostr_index) { pending_derivation = 1; if (selector_req.nostr_index == 0) { json_copy_string(role_name, sizeof(role_name), "main", "main"); } else { (void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index); } json_copy_string(purpose, sizeof(purpose), "nostr", "nostr"); } else if (selector_rc == SELECTOR_ERR_AMBIGUOUS || selector_rc == SELECTOR_ERR_NOT_FOUND || selector_rc == SELECTOR_ERR_NO_DEFAULT) { hard_selector_error = selector_rc; } } } if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}"); pchk = POLICY_DENY; } else if (hard_selector_error == SELECTOR_ERR_NO_DEFAULT) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":1003,\"message\":\"no_default_role\"}}"); pchk = POLICY_DENY; } else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}"); pchk = POLICY_DENY; } else { pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src); } if (pchk == POLICY_PROMPT) { pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation); if (pchk == POLICY_ALLOW_SESSION_VERB || pchk == POLICY_ALLOW_SESSION_ALL) { policy_entry_t grant; memset(&grant, 0, sizeof(grant)); strncpy(grant.caller, caller.caller_id, sizeof(grant.caller) - 1); strncpy(grant.roles[0], role_name, ROLE_NAME_MAX - 1); grant.role_count = 1; if (pchk == POLICY_ALLOW_SESSION_VERB) { strncpy(grant.verbs[0], method, POLICY_VERB_MAX_LEN - 1); grant.verb_count = 1; } grant.prompt = PROMPT_NEVER; grant.source = POLICY_SOURCE_SESSION_GRANT; if (policy_table_insert_before_last(ctx->policy, &grant) != 0) { pchk = POLICY_DENY; } else { pchk = POLICY_ALLOW; } } } if (pchk == POLICY_ALLOW && pending_derivation) { role_entry_t *new_role; int role_index; if (ctx->dispatcher == NULL || ctx->dispatcher->role_table == NULL || ctx->dispatcher->key_store == NULL || ctx->dispatcher->mnemonic == NULL || role_table_register_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index) != 0) { derivation_error = 1; } else { new_role = role_table_find_by_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index); if (new_role == NULL) { derivation_error = 1; } else { role_index = (int)(new_role - &ctx->dispatcher->role_table->entries[0]); if (role_index < 0 || role_index >= ctx->dispatcher->role_table->count || crypto_derive_one(ctx->dispatcher->key_store, ctx->dispatcher->role_table, ctx->dispatcher->mnemonic, role_index) != 0) { derivation_error = 1; } else { json_copy_string(role_name, sizeof(role_name), new_role->name, role_name); json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(new_role->purpose), "nostr"); } } } } if (pchk == POLICY_ALLOW && !derivation_error) { verdict = "ALLOWED"; response = dispatcher_handle_request(ctx->dispatcher, request); if (response == NULL) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}"); } } else if (pchk == POLICY_ALLOW && derivation_error) { verdict = "DENIED"; response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}"); } else { verdict = "DENIED"; if (response == NULL) { response = strdup("{\"id\":\"null\",\"error\":{\"code\":2001,\"message\":\"policy_denied\"}}"); } } if (response != NULL) { (void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response); } if (pchk == POLICY_ALLOW && policy_src == POLICY_SOURCE_PREAPPROVE) { source_label = "preapprove"; } else if (pchk == POLICY_ALLOW && policy_src == POLICY_SOURCE_SESSION_GRANT) { source_label = "session-grant"; } else if (pchk == POLICY_ALLOW) { source_label = "prompt"; } else { source_label = "no-match"; } (void)snprintf(activity, sizeof(activity), "%s %s(%s) %s:%s", caller.caller_id, method, role_name, verdict, source_label); if (cb != NULL) { cb(activity, cb_data); } free(request); free(response); if (client_fd != STDIN_FILENO) { close(client_fd); } return 1; } void server_stop(server_ctx_t *ctx) { if (ctx == NULL) { return; } if (ctx->listen_fd >= 0) { if (ctx->listen_mode == NSIGNER_LISTEN_UNIX || ctx->listen_mode == NSIGNER_LISTEN_TCP) { close(ctx->listen_fd); } ctx->listen_fd = -1; } ctx->running = 0; }