Files
n_signer/src/server.c

1512 lines
48 KiB
C

#define _GNU_SOURCE
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <cJSON.h>
/* 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 80
/* 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": <int>, "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
/* Caller identity */
typedef struct {
uid_t uid;
gid_t gid;
pid_t pid;
int kind;
char caller_id[80]; /* "uid:<n>", "qubes:<vm>", or "pubkey:<hex>" */
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;
} 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,
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_<word1>_<word2>
* 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 <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
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 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,
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;
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';
if (caller.kind == NSIGNER_LISTEN_TCP) {
if (auth_envelope_verify_request(request,
&g_auth_nonce_cache,
AUTH_DEFAULT_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,
"");
(void)snprintf(caller.caller_id, sizeof(caller.caller_id), "pubkey:%s", 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;
}