#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 #define POLICY_ALLOW_SESSION_ALL 4 #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); /* Parse a --preapprove spec into a policy entry. */ int parse_preapprove_spec(const char *spec, policy_entry_t *out_entry, int *out_nostr_index); /* * 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); /* 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:" or "qubes:" */ char source_qube[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); /* Configure interactive policy-prompt behavior for current session */ void server_set_prompt_always_allow(int enabled); /* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */ void server_set_noninteractive_prompt_default(int decision); typedef struct { const caller_identity_t *caller; const char *method; const char *role_name; const char *purpose; int pending_derivation; const char *fips_peer_npub; const char *fips_peer_name; } server_approval_request_t; typedef int (*server_approval_cb)(const server_approval_request_t *req, void *user_data); void server_set_approval_cb(server_approval_cb cb, void *user_data); /* 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 29 #define NSIGNER_VERSION "v0.0.29" /* 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 "tui_continuous.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define NSIGNER_DEFAULT_SOCKET_NAME "nsigner" #define ACTIVITY_LOG_CAP 16 #define CONNECTION_INFO_CAP 8 typedef struct { char lines[ACTIVITY_LOG_CAP][256]; int count; } activity_log_t; static volatile sig_atomic_t g_running = 1; static activity_log_t g_activity_log; static int g_auto_approve = 0; typedef enum { MNEMONIC_SOURCE_TUI = 0, MNEMONIC_SOURCE_STDIN, MNEMONIC_SOURCE_FD } mnemonic_source_kind_t; typedef struct { mnemonic_source_kind_t kind; int fd; } mnemonic_source_t; static mnemonic_source_kind_t g_startup_mnemonic_source_kind = MNEMONIC_SOURCE_TUI; static char g_connection_info[CONNECTION_INFO_CAP][192]; static int g_connection_info_count = 0; static const TuiMenuItem g_main_menu_items[] = { {"^_q^:/x quit", 'q'}, {"^_l^: lock/reunlock", 'l'}, {"^_r^: refresh", 'r'}, {"^_A^: toggle auto-approve", 'a'} }; typedef struct { const role_table_t *role_table; } role_table_view_data_t; typedef struct { const role_table_t *role_table; const mnemonic_state_t *mnemonic; int *derived_count; const char *socket_name; } main_tui_context_t; static void render_status(const role_table_t *role_table, const mnemonic_state_t *mnemonic, int derived_count, const char *socket_name); static void connection_info_clear(void) { g_connection_info_count = 0; memset(g_connection_info, 0, sizeof(g_connection_info)); } static void connection_info_add(const char *fmt, ...) { va_list ap; if (fmt == NULL || g_connection_info_count >= CONNECTION_INFO_CAP) { return; } va_start(ap, fmt); (void)vsnprintf(g_connection_info[g_connection_info_count], sizeof(g_connection_info[g_connection_info_count]), fmt, ap); va_end(ap); g_connection_info_count++; } static void handle_signal(int sig) { (void)sig; g_running = 0; } static int read_line_stdin(char *buf, size_t buf_sz) { size_t len; if (buf == NULL || buf_sz == 0) { return -1; } if (fgets(buf, (int)buf_sz, stdin) == NULL) { return -1; } len = strlen(buf); while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { buf[len - 1] = '\0'; len--; } return 0; } static int read_cmd_output_local(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_local_fips_identity(char *out_ipv6, size_t out_ipv6_sz, char *out_npub, size_t out_npub_sz) { char *json = NULL; cJSON *root = NULL; cJSON *ipv6_item; cJSON *npub_item; if (out_ipv6 == NULL || out_ipv6_sz == 0 || out_npub == NULL || out_npub_sz == 0) { return -1; } out_ipv6[0] = '\0'; out_npub[0] = '\0'; if (read_cmd_output_local("fipsctl show status 2>/dev/null", &json) != 0) { return -1; } root = cJSON_Parse(json); free(json); if (root == NULL) { return -1; } ipv6_item = cJSON_GetObjectItemCaseSensitive(root, "ipv6_addr"); npub_item = cJSON_GetObjectItemCaseSensitive(root, "npub"); if (cJSON_IsString(ipv6_item) && ipv6_item->valuestring != NULL) { strncpy(out_ipv6, ipv6_item->valuestring, out_ipv6_sz - 1); out_ipv6[out_ipv6_sz - 1] = '\0'; } if (cJSON_IsString(npub_item) && npub_item->valuestring != NULL) { strncpy(out_npub, npub_item->valuestring, out_npub_sz - 1); out_npub[out_npub_sz - 1] = '\0'; } cJSON_Delete(root); return (out_ipv6[0] != '\0' && out_npub[0] != '\0') ? 0 : -1; } static int extract_listen_port(const char *listen_target, char *out_port, size_t out_port_sz) { const char *port; size_t i; if (listen_target == NULL || out_port == NULL || out_port_sz == 0) { return -1; } out_port[0] = '\0'; port = strrchr(listen_target, ':'); if (port == NULL || *(port + 1) == '\0') { return -1; } port++; for (i = 0; port[i] != '\0'; ++i) { if (!isdigit((unsigned char)port[i])) { return -1; } } strncpy(out_port, port, out_port_sz - 1); out_port[out_port_sz - 1] = '\0'; return 0; } static int connect_abstract_socket(const char *name) { int fd; struct sockaddr_un addr; socklen_t addr_len; if (name == NULL || name[0] == '\0') { return -1; } fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { return -1; } memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; addr.sun_path[0] = '\0'; strncpy(&addr.sun_path[1], 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(name)); if (connect(fd, (struct sockaddr *)&addr, addr_len) != 0) { close(fd); return -1; } return fd; } static void print_usage(const char *program_name) { tui_print("nsigner - single-binary signer program"); tui_print("Usage:"); tui_print(" %s [--socket-name|--name|-n ] [--listen|-l ]", program_name); tui_print(" [--preapprove|-p ]... [--auth|-a ]"); tui_print(" [--mnemonic-stdin|--mnemonic-fd ] [--allow-all|-A]"); tui_print(" Run signer server (unix mode has TUI)"); tui_print(" %s [--socket-name|--name|-n ] client '' Send JSON-RPC request", program_name); tui_print(" %s [--socket-name|--name|-n ] client - Read JSON-RPC request from stdin", program_name); tui_print(" %s list List running nsigner abstract sockets", program_name); tui_print(" %s --help", program_name); tui_print(" %s --version", program_name); tui_print(""); tui_print("Options:"); tui_print(" --listen, -l MODE Listener transport: unix|stdio|qrexec|tcp:HOST:PORT"); tui_print(" --preapprove, -p SPEC"); tui_print(" Pre-approve a caller for a role (repeatable)"); tui_print(" SPEC: caller=,role= or caller=,nostr_index="); tui_print(" --auth, -a MODE Auth envelope policy per listener: off|optional|required"); tui_print(" --mnemonic-stdin Read mnemonic from stdin (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"); } static int extract_nsigner_socket_from_proc_line(const char *line, char *with_at, size_t with_at_sz, char *without_at, size_t without_at_sz) { const char *p; const char *end; size_t len_with_at; if (line == NULL) { return -1; } p = strstr(line, "@nsigner"); if (p == NULL) { return -1; } if (p[8] != '\0' && p[8] != '\n' && p[8] != ' ' && p[8] != '\t' && p[8] != '_') { return -1; } end = p; while (*end != '\0' && *end != '\n' && *end != ' ' && *end != '\t') { end++; } len_with_at = (size_t)(end - p); if (len_with_at <= 1 || len_with_at > SERVER_SOCKET_NAME_MAX) { return -1; } if (with_at != NULL && with_at_sz > 0) { size_t copy_len = (len_with_at < (with_at_sz - 1)) ? len_with_at : (with_at_sz - 1); memcpy(with_at, p, copy_len); with_at[copy_len] = '\0'; } if (without_at != NULL && without_at_sz > 0) { size_t len_without_at = len_with_at - 1; size_t copy_len = (len_without_at < (without_at_sz - 1)) ? len_without_at : (without_at_sz - 1); memcpy(without_at, p + 1, copy_len); without_at[copy_len] = '\0'; } return 0; } static int list_sockets_main(void) { FILE *fp; char line[512]; int found = 0; fp = fopen("/proc/net/unix", "r"); if (fp == NULL) { perror("fopen(/proc/net/unix)"); return 1; } while (fgets(line, sizeof(line), fp) != NULL) { char name_with_at[SERVER_SOCKET_NAME_MAX + 1]; if (extract_nsigner_socket_from_proc_line(line, name_with_at, sizeof(name_with_at), NULL, 0) == 0) { printf("%s\n", name_with_at); found = 1; } } fclose(fp); if (!found) { printf("(none)\n"); } return 0; } static int discover_single_socket_name(char *out, size_t out_len) { FILE *fp; char line[512]; int count = 0; if (out == NULL || out_len == 0) { return -1; } out[0] = '\0'; fp = fopen("/proc/net/unix", "r"); if (fp == NULL) { return -1; } while (fgets(line, sizeof(line), fp) != NULL) { char name_no_at[SERVER_SOCKET_NAME_MAX + 1]; if (extract_nsigner_socket_from_proc_line(line, NULL, 0, name_no_at, sizeof(name_no_at)) != 0) { continue; } count++; if (count == 1) { strncpy(out, name_no_at, out_len - 1); out[out_len - 1] = '\0'; } } fclose(fp); return (count == 1 && out[0] != '\0') ? 0 : -1; } static int client_main(int argc, char *argv[], const char *socket_name, int socket_name_explicit) { int fd; char *request = NULL; char *response = NULL; char stdin_buf[SERVER_MAX_MSG_SIZE + 1]; if (argc < 1) { fprintf(stderr, "Usage: nsigner [--socket-name|--name|-n ] client '' | -\n"); return 1; } if (strcmp(argv[0], "-") == 0) { if (read_line_stdin(stdin_buf, sizeof(stdin_buf)) != 0) { fprintf(stderr, "Failed to read request from stdin\n"); return 1; } request = stdin_buf; } else { request = argv[0]; } if (!socket_name_explicit) { char discovered[SERVER_SOCKET_NAME_MAX]; if (discover_single_socket_name(discovered, sizeof(discovered)) == 0) { socket_name = discovered; } } fd = connect_abstract_socket(socket_name); if (fd < 0) { fprintf(stderr, "Failed to connect to @%s: %s\n", socket_name, strerror(errno)); return 1; } if (transport_send_framed(fd, request) != 0) { perror("send"); close(fd); return 1; } if (transport_recv_framed(fd, &response, SERVER_MAX_MSG_SIZE) != 0) { perror("recv"); close(fd); return 1; } printf("%s\n", response); free(response); close(fd); return 0; } static void activity_log_cb(const char *message, void *user_data) { int idx; time_t now; struct tm tm_now; char ts[16]; (void)user_data; if (message == NULL) { return; } now = time(NULL); if (localtime_r(&now, &tm_now) != NULL) { (void)strftime(ts, sizeof(ts), "%m%d-%H%M%S", &tm_now); } else { strncpy(ts, "0000-000000", sizeof(ts) - 1); ts[sizeof(ts) - 1] = '\0'; } idx = g_activity_log.count % ACTIVITY_LOG_CAP; (void)snprintf(g_activity_log.lines[idx], sizeof(g_activity_log.lines[idx]), "%s %s", ts, message); g_activity_log.count++; } static void tcp_activity_stdout_cb(const char *message, void *user_data) { time_t now; struct tm tm_now; char ts[32]; (void)user_data; if (message == NULL) { return; } now = time(NULL); if (localtime_r(&now, &tm_now) != NULL) { (void)strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_now); } else { strncpy(ts, "0000-00-00 00:00:00", sizeof(ts) - 1); ts[sizeof(ts) - 1] = '\0'; } printf("[%s] %s\n", ts, message); fflush(stdout); } static void role_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) { const role_table_view_data_t *view = (const role_table_view_data_t *)user_data; const role_entry_t *r; if (out == NULL || out_size == 0 || view == NULL || view->role_table == NULL || row < 0 || row >= view->role_table->count) { return; } r = &view->role_table->entries[row]; switch (col) { case 0: (void)snprintf(out, out_size, "%s", r->name); break; case 1: (void)snprintf(out, out_size, "%s", role_purpose_to_str(r->purpose)); break; case 2: (void)snprintf(out, out_size, "%s", role_curve_to_str(r->curve)); break; case 3: (void)snprintf(out, out_size, "%s", (r->selector_type == SELECTOR_NOSTR_INDEX) ? "nostr_index" : "role_path"); break; default: out[0] = '\0'; break; } } static void render_status(const role_table_t *role_table, const mnemonic_state_t *mnemonic, int derived_count, const char *socket_name) { TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Main Menu" }; TuiMenu menu = { g_main_menu_items, (int)(sizeof(g_main_menu_items) / sizeof(g_main_menu_items[0])) }; TuiSize size = tui_terminal_size(); int left_col = tui_menu_left_col(&frame, size.width); char status_buf[256]; tui_clear_continuous(size.height); tui_render_top_frame(&frame, size.width); tui_print("^*Connections^:"); if (g_connection_info_count == 0) { tui_print("(none)"); } else { for (int i = 0; i < g_connection_info_count; ++i) { tui_print("%s", g_connection_info[i]); } } tui_print(""); tui_render_menu(&menu, left_col); tui_print(""); (void)snprintf(status_buf, sizeof(status_buf), "session=%s (%d words) signer=%s derived=%d auto-approve=%s", mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked", (mnemonic != NULL) ? mnemonic->word_count : 0, (socket_name != NULL) ? socket_name : "(none)", derived_count, g_auto_approve ? "ON" : "OFF"); tui_print("%s", status_buf); tui_print("^*Roles^:"); if (role_table == NULL || role_table->count == 0) { tui_print("(none)"); } else { static const TuiColumn columns[] = { {"Role", 20, 0}, {"Purpose", 12, 0}, {"Curve", 12, 0}, {"Selector", 12, 0} }; role_table_view_data_t view; TuiTable table; view.role_table = role_table; table.columns = columns; table.column_count = (int)(sizeof(columns) / sizeof(columns[0])); table.row_count = role_table->count; table.user_data = &view; table.get_cell = role_table_get_cell; table.is_default = NULL; table.prefix_len = NULL; tui_render_table(&table); } tui_print(""); tui_print("^*Activity (latest first)^:"); if (g_activity_log.count == 0) { tui_print("(none)"); } else { int i; int shown = 0; for (i = g_activity_log.count - 1; i >= 0 && shown < ACTIVITY_LOG_CAP; --i, ++shown) { tui_print("%s", g_activity_log.lines[i % ACTIVITY_LOG_CAP]); } } tui_anchor_prompt(0, left_col); fflush(stdout); } static int tui_approval_cb(const server_approval_request_t *req, void *user_data) { main_tui_context_t *ctx = (main_tui_context_t *)user_data; TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Approval" }; char choice[32]; int decision = POLICY_DENY; tui_render_content_screen(&frame, "Approval required"); tui_print("caller: %s", (req != NULL && req->caller != NULL) ? req->caller->caller_id : "unknown"); if (req != NULL && req->fips_peer_npub != NULL) { if (req->fips_peer_name != NULL && req->fips_peer_name[0] != '\0') { tui_print("fips peer: %s (%s)", req->fips_peer_npub, req->fips_peer_name); } else { tui_print("fips peer: %s", req->fips_peer_npub); } } tui_print("method: %s", (req != NULL && req->method != NULL) ? req->method : "unknown"); tui_print("role: %s", (req != NULL && req->role_name != NULL) ? req->role_name : "unknown"); tui_print("purpose: %s", (req != NULL && req->purpose != NULL) ? req->purpose : "unknown"); if (req != NULL && req->pending_derivation) { tui_print("** NEW IDENTITY — will be derived if approved **"); } tui_print(""); tui_print("^_y^: allow once"); tui_print("^_n^: deny"); tui_print("^_e^: allow this caller+role+verb for session"); tui_print("^_a^: allow this caller+role for session (all verbs)"); printf("> "); fflush(stdout); if (read_line_stdin(choice, sizeof(choice)) == 0) { char ch = (char)tolower((unsigned char)choice[0]); if (ch == 'a') { decision = POLICY_ALLOW_SESSION_ALL; } else if (ch == 'e') { decision = POLICY_ALLOW_SESSION_VERB; } else if (ch == 'y') { decision = POLICY_ALLOW; } } if (ctx != NULL) { render_status(ctx->role_table, ctx->mnemonic, (ctx->derived_count != NULL) ? *ctx->derived_count : 0, ctx->socket_name); } return decision; } static int setup_default_role(role_table_t *role_table) { role_entry_t role; if (role_table == NULL) { return -1; } memset(&role, 0, sizeof(role)); strncpy(role.name, "main", sizeof(role.name) - 1); strncpy(role.purpose_str, "nostr", sizeof(role.purpose_str) - 1); strncpy(role.curve_str, "secp256k1", sizeof(role.curve_str) - 1); role.purpose = role_purpose_from_str(role.purpose_str); role.curve = role_curve_from_str(role.curve_str); role.selector_type = SELECTOR_NOSTR_INDEX; role.nostr_index = 0; role.derived = 0; return role_table_add(role_table, &role); } static int prompt_load_mnemonic_tui(mnemonic_state_t *mnemonic) { char phrase[MNEMONIC_MAX_LEN]; char phrase_copy[MNEMONIC_MAX_LEN]; char mode[MNEMONIC_MAX_LEN]; int invalid_attempts = 0; const int max_invalid_attempts = 10; if (mnemonic == NULL) { return -1; } while (invalid_attempts < max_invalid_attempts) { { TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Unlock" }; tui_render_content_screen(&frame, "Load mnemonic"); tui_print("Mnemonic source: [E]nter existing or [G]enerate new"); tui_print("Default is E; you can also paste full mnemonic here."); printf("> "); fflush(stdout); } if (read_line_stdin(mode, sizeof(mode)) != 0) { fprintf(stderr, "Failed to read mnemonic source choice\n"); return -1; } if ((mode[0] == 'q' || mode[0] == 'Q' || mode[0] == 'x' || mode[0] == 'X') && mode[1] == '\0') { fprintf(stderr, "User requested exit.\n"); return -1; } if (strchr(mode, ' ') != NULL && mode[0] != 'g' && mode[0] != 'G') { if (mnemonic_load(mnemonic, mode) == 0) { printf("Seed phrase is valid and accepted.\n"); return 0; } invalid_attempts++; fprintf(stderr, "Invalid mnemonic (must be 12/15/18/21/24 words). Attempts: %d/%d\n", invalid_attempts, max_invalid_attempts); continue; } if (mode[0] == 'g' || mode[0] == 'G') { int idx = 1; char *ctx = NULL; char *word; if (mnemonic_generate(12, phrase, sizeof(phrase)) != 0) { fprintf(stderr, "Failed to generate mnemonic\n"); return -1; } strncpy(phrase_copy, phrase, sizeof(phrase_copy) - 1); phrase_copy[sizeof(phrase_copy) - 1] = '\0'; tui_print(""); tui_print("Generated mnemonic (WRITE THIS DOWN - it will not be shown again):"); word = strtok_r(phrase_copy, " ", &ctx); while (word != NULL) { printf("%2d. %s\n", idx, word); idx++; word = strtok_r(NULL, " ", &ctx); } tui_print(""); tui_print("Press Enter after writing down your mnemonic to continue."); printf("> "); fflush(stdout); if (read_line_stdin(mode, sizeof(mode)) != 0) { fprintf(stderr, "Failed to read continue input\n"); memset(phrase, 0, sizeof(phrase)); memset(phrase_copy, 0, sizeof(phrase_copy)); return -1; } if (mnemonic_load(mnemonic, phrase) != 0) { memset(phrase, 0, sizeof(phrase)); memset(phrase_copy, 0, sizeof(phrase_copy)); fprintf(stderr, "Failed to load generated mnemonic\n"); return -1; } printf("Seed phrase is valid and accepted.\n"); memset(phrase, 0, sizeof(phrase)); memset(phrase_copy, 0, sizeof(phrase_copy)); return 0; } { TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Unlock" }; tui_render_content_screen(&frame, "Enter mnemonic"); tui_print("Enter mnemonic (12/15/18/21/24 words):"); printf("> "); fflush(stdout); } if (read_line_stdin(phrase, sizeof(phrase)) != 0) { fprintf(stderr, "Failed to read mnemonic\n"); return -1; } if ((phrase[0] == 'q' || phrase[0] == 'Q' || phrase[0] == 'x' || phrase[0] == 'X') && phrase[1] == '\0') { memset(phrase, 0, sizeof(phrase)); fprintf(stderr, "User requested exit.\n"); return -1; } if (mnemonic_load(mnemonic, phrase) == 0) { printf("Seed phrase is valid and accepted.\n"); memset(phrase, 0, sizeof(phrase)); return 0; } memset(phrase, 0, sizeof(phrase)); invalid_attempts++; fprintf(stderr, "Invalid mnemonic (must be 12/15/18/21/24 words). Attempts: %d/%d\n", invalid_attempts, max_invalid_attempts); } fprintf(stderr, "Too many invalid mnemonic attempts (%d). Exiting.\n", max_invalid_attempts); return -1; } static int read_mnemonic_from_fd(int fd, char *out, size_t out_sz) { size_t len = 0; int saw_newline = 0; if (fd < 0 || out == NULL || out_sz < 2) { return -1; } for (;;) { unsigned char ch; ssize_t n = read(fd, &ch, 1); if (n == 0) { break; } if (n < 0) { if (errno == EINTR) { continue; } return -1; } if (ch == '\n') { saw_newline = 1; break; } if (ch == '\0') { return -2; } if (len + 1 >= out_sz) { return -3; } out[len++] = (char)ch; } out[len] = '\0'; if (len > 0 && out[len - 1] == '\r') { out[len - 1] = '\0'; } (void)saw_newline; return 0; } static int replace_stdin_with_devnull(void) { int null_fd = open("/dev/null", O_RDONLY); int rc = 0; if (null_fd < 0) { return -1; } if (dup2(null_fd, STDIN_FILENO) < 0) { rc = -1; } close(null_fd); return rc; } static int load_mnemonic(mnemonic_state_t *mnemonic, const mnemonic_source_t *source) { char phrase[MNEMONIC_MAX_LEN]; int rc = -1; if (mnemonic == NULL || source == NULL) { return -1; } if (source->kind == MNEMONIC_SOURCE_TUI) { return prompt_load_mnemonic_tui(mnemonic); } memset(phrase, 0, sizeof(phrase)); rc = read_mnemonic_from_fd(source->fd, phrase, sizeof(phrase)); if (rc != 0) { if (rc == -2) { fprintf(stderr, "nsigner: mnemonic input contains embedded NUL byte\n"); } else if (rc == -3) { fprintf(stderr, "nsigner: mnemonic input exceeded maximum length\n"); } else { fprintf(stderr, "nsigner: failed to read mnemonic from fd %d: %s\n", source->fd, strerror(errno)); } if (source->fd >= 0) { close(source->fd); } secure_memzero(phrase, sizeof(phrase)); return -1; } rc = mnemonic_load(mnemonic, phrase); secure_memzero(phrase, sizeof(phrase)); if (source->fd >= 0) { close(source->fd); } if (rc != 0) { fprintf(stderr, "nsigner: mnemonic validation failed\n"); return -1; } if (source->kind == MNEMONIC_SOURCE_STDIN || source->fd == STDIN_FILENO) { if (replace_stdin_with_devnull() != 0) { fprintf(stderr, "nsigner: failed to rebind stdin to /dev/null: %s\n", strerror(errno)); mnemonic_unload(mnemonic); return -1; } } printf("Seed phrase is valid and accepted.\n"); return 0; } static void apply_test_overrides(policy_table_t *policy) { const char *force_prompt; const char *noninteractive_prompt; const char *hotkeys; if (policy == NULL) { return; } force_prompt = getenv("NSIGNER_TEST_FORCE_PROMPT"); if (force_prompt != NULL && strcmp(force_prompt, "1") == 0) { policy_entry_t e; uid_t uid = getuid(); memset(&e, 0, sizeof(e)); (void)snprintf(e.caller, sizeof(e.caller), "uid:%u", (unsigned int)uid); e.prompt = PROMPT_EVERY_REQUEST; (void)policy_table_add(policy, &e); } noninteractive_prompt = getenv("NSIGNER_TEST_NONINTERACTIVE_PROMPT"); if (noninteractive_prompt != NULL) { if (strcasecmp(noninteractive_prompt, "allow") == 0) { server_set_noninteractive_prompt_default(POLICY_ALLOW); } else if (strcasecmp(noninteractive_prompt, "deny") == 0) { server_set_noninteractive_prompt_default(POLICY_DENY); } } hotkeys = getenv("NSIGNER_TEST_HOTKEYS"); if (hotkeys != NULL) { const char *p = hotkeys; while (*p != '\0') { char ch = *p; if (ch == 'A') { g_auto_approve = g_auto_approve ? 0 : 1; server_set_prompt_always_allow(g_auto_approve); } p++; } } } int main(int argc, char *argv[]) { mnemonic_state_t mnemonic; role_table_t role_table; dispatcher_ctx_t dispatcher; policy_table_t policy; server_ctx_t server; key_store_t key_store; struct pollfd pfds[2]; uid_t owner_uid; int derived_count; const char *socket_name = NSIGNER_DEFAULT_SOCKET_NAME; char generated_socket_name[SERVER_SOCKET_NAME_MAX]; int socket_name_explicit = 0; int listen_mode = NSIGNER_LISTEN_UNIX; const char *listen_target = NSIGNER_DEFAULT_SOCKET_NAME; int argi = 1; const char *preapprove_specs[POLICY_MAX_ENTRIES]; int preapprove_count = 0; int auth_mode = NSIGNER_AUTH_OFF; int auth_skew_seconds = AUTH_DEFAULT_SKEW_SECONDS; int allow_all = 0; mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 }; while (argi < argc) { if (strcmp(argv[argi], "--socket-name") == 0 || strcmp(argv[argi], "--name") == 0 || strcmp(argv[argi], "-n") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } socket_name = argv[argi + 1]; socket_name_explicit = 1; argi += 2; continue; } if (strcmp(argv[argi], "--listen") == 0 || strcmp(argv[argi], "-l") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } if (strcmp(argv[argi + 1], "unix") == 0) { listen_mode = NSIGNER_LISTEN_UNIX; } else if (strcmp(argv[argi + 1], "stdio") == 0) { listen_mode = NSIGNER_LISTEN_STDIO; } else if (strcmp(argv[argi + 1], "qrexec") == 0) { listen_mode = NSIGNER_LISTEN_QREXEC; } else if (strncmp(argv[argi + 1], "tcp:", 4) == 0) { listen_mode = NSIGNER_LISTEN_TCP; listen_target = argv[argi + 1]; } else { fprintf(stderr, "Invalid --listen mode: %s (expected unix|stdio|qrexec|tcp:HOST:PORT)\n", argv[argi + 1]); return 1; } argi += 2; continue; } if (strcmp(argv[argi], "--auth") == 0 || strcmp(argv[argi], "-a") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } if (strcmp(argv[argi + 1], "off") == 0) { auth_mode = NSIGNER_AUTH_OFF; } else if (strcmp(argv[argi + 1], "optional") == 0) { auth_mode = NSIGNER_AUTH_OPTIONAL; } else if (strcmp(argv[argi + 1], "required") == 0) { auth_mode = NSIGNER_AUTH_REQUIRED; } else { fprintf(stderr, "Invalid --auth mode: %s (expected off|optional|required)\n", argv[argi + 1]); return 1; } argi += 2; continue; } if (strcmp(argv[argi], "--preapprove") == 0 || strcmp(argv[argi], "-p") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } if (preapprove_count >= POLICY_MAX_ENTRIES) { fprintf(stderr, "Too many --preapprove entries (max %d)\n", POLICY_MAX_ENTRIES); return 1; } preapprove_specs[preapprove_count++] = argv[argi + 1]; argi += 2; continue; } if (strcmp(argv[argi], "--mnemonic-stdin") == 0) { if (mnemonic_source.kind != MNEMONIC_SOURCE_TUI) { fprintf(stderr, "nsigner: --mnemonic-stdin and --mnemonic-fd are mutually exclusive\n"); return 1; } mnemonic_source.kind = MNEMONIC_SOURCE_STDIN; mnemonic_source.fd = STDIN_FILENO; argi += 1; continue; } if (strcmp(argv[argi], "--mnemonic-fd") == 0) { char *endp = NULL; long parsed_fd; if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } if (mnemonic_source.kind != MNEMONIC_SOURCE_TUI) { fprintf(stderr, "nsigner: --mnemonic-stdin and --mnemonic-fd are mutually exclusive\n"); return 1; } errno = 0; parsed_fd = strtol(argv[argi + 1], &endp, 10); if (errno != 0 || endp == argv[argi + 1] || *endp != '\0' || parsed_fd < 0 || parsed_fd > INT_MAX) { fprintf(stderr, "nsigner: --mnemonic-fd %s: invalid FD value\n", argv[argi + 1]); return 1; } mnemonic_source.kind = MNEMONIC_SOURCE_FD; mnemonic_source.fd = (int)parsed_fd; argi += 2; continue; } if (strcmp(argv[argi], "--allow-all") == 0 || strcmp(argv[argi], "-A") == 0) { allow_all = 1; argi += 1; continue; } break; } if (argi < argc && strcmp(argv[argi], "client") == 0) { if (listen_mode != NSIGNER_LISTEN_UNIX) { fprintf(stderr, "--listen is server-only; client mode uses unix abstract sockets\n"); return 1; } return client_main(argc - argi - 1, argv + argi + 1, socket_name, socket_name_explicit); } if (argi < argc && strcmp(argv[argi], "list") == 0) { if (listen_mode != NSIGNER_LISTEN_UNIX) { fprintf(stderr, "--listen is server-only; list inspects unix abstract sockets\n"); return 1; } return list_sockets_main(); } if (argi < argc && (strcmp(argv[argi], "--version") == 0 || strcmp(argv[argi], "-v") == 0)) { printf("nsigner %s\n", NSIGNER_VERSION); return 0; } if (argi < argc && (strcmp(argv[argi], "--help") == 0 || strcmp(argv[argi], "-h") == 0)) { print_usage(argv[0]); return 0; } if (argi < argc) { fprintf(stderr, "Unknown argument: %s\n", argv[argi]); print_usage(argv[0]); return 1; } if (mnemonic_source.kind == MNEMONIC_SOURCE_STDIN && (listen_mode == NSIGNER_LISTEN_STDIO || listen_mode == NSIGNER_LISTEN_QREXEC)) { fprintf(stderr, "nsigner: --mnemonic-stdin requires --listen unix or --listen tcp:...\n"); return 1; } if (mnemonic_source.kind == MNEMONIC_SOURCE_FD) { if (mnemonic_source.fd == STDOUT_FILENO) { fprintf(stderr, "nsigner: --mnemonic-fd 1 is not allowed\n"); return 1; } if (mnemonic_source.fd == STDIN_FILENO && (listen_mode == NSIGNER_LISTEN_STDIO || listen_mode == NSIGNER_LISTEN_QREXEC)) { fprintf(stderr, "nsigner: --mnemonic-fd 0 cannot be used with --listen stdio or --listen qrexec\n"); return 1; } } printf("nsigner %s\n", NSIGNER_VERSION); fflush(stdout); g_startup_mnemonic_source_kind = mnemonic_source.kind; mnemonic_init(&mnemonic); if (load_mnemonic(&mnemonic, &mnemonic_source) != 0) { mnemonic_unload(&mnemonic); return 1; } role_table_init(&role_table); if (setup_default_role(&role_table) != 0) { fprintf(stderr, "Failed to initialize default role\n"); mnemonic_unload(&mnemonic); return 1; } memset(&key_store, 0, sizeof(key_store)); owner_uid = getuid(); policy_init_default(&policy, owner_uid); for (int i = 0; i < preapprove_count; ++i) { policy_entry_t entry; int parsed_nostr_index = -1; if (parse_preapprove_spec(preapprove_specs[i], &entry, &parsed_nostr_index) != 0) { mnemonic_unload(&mnemonic); return 1; } entry.source = POLICY_SOURCE_PREAPPROVE; if (parsed_nostr_index >= 0 && role_table_find_by_nostr_index(&role_table, parsed_nostr_index) == NULL) { if (role_table_register_nostr_index(&role_table, parsed_nostr_index) != 0) { fprintf(stderr, "ERROR: failed to register role for --preapprove nostr_index=%d\n", parsed_nostr_index); mnemonic_unload(&mnemonic); return 1; } } if (policy_table_insert_before_last(&policy, &entry) != 0) { fprintf(stderr, "ERROR: failed to insert --preapprove policy entry (table full): %s\n", preapprove_specs[i]); mnemonic_unload(&mnemonic); return 1; } fprintf(stderr, "[PREAPPROVE] caller=%s role=%s\n", entry.caller, entry.roles[0]); } apply_test_overrides(&policy); if (allow_all) { server_set_prompt_always_allow(1); } if (nostr_init() != 0) { fprintf(stderr, "Failed to initialize crypto subsystem\n"); mnemonic_unload(&mnemonic); return 1; } derived_count = crypto_derive_all(&key_store, &role_table, &mnemonic); if (derived_count < 0) { fprintf(stderr, "Failed to derive keys from mnemonic\n"); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } dispatcher_init(&dispatcher, &role_table, &mnemonic, &key_store); if (listen_mode == NSIGNER_LISTEN_UNIX && !socket_name_explicit) { if (socket_name_random(generated_socket_name, sizeof(generated_socket_name)) != 0) { fprintf(stderr, "Failed to generate random socket name\n"); crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } socket_name = generated_socket_name; } if (listen_mode == NSIGNER_LISTEN_UNIX) { listen_target = socket_name; } else if (listen_mode == NSIGNER_LISTEN_TCP && socket_name_explicit) { fprintf(stderr, "--socket-name is only valid with unix listen mode\n"); crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } if ((listen_mode == NSIGNER_LISTEN_UNIX || listen_mode == NSIGNER_LISTEN_STDIO) && auth_mode != NSIGNER_AUTH_OFF) { fprintf(stderr, "--auth is currently supported only with --listen qrexec (optional/required) and tcp (required)\n"); crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } if (listen_mode == NSIGNER_LISTEN_TCP) { auth_mode = NSIGNER_AUTH_REQUIRED; } server_init(&server, listen_target, socket_name_explicit, listen_mode, auth_mode, auth_skew_seconds, &dispatcher, &policy); if (server_start(&server) != 0) { if (listen_mode == NSIGNER_LISTEN_UNIX) { fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server)); } else if (listen_mode == NSIGNER_LISTEN_TCP) { fprintf(stderr, "Failed to start server on %s: %s\n", listen_target, server_last_error(&server)); } else { fprintf(stderr, "Failed to start server (%s): %s\n", (listen_mode == NSIGNER_LISTEN_QREXEC) ? "qrexec" : "stdio", server_last_error(&server)); } crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } (void)signal(SIGINT, handle_signal); (void)signal(SIGTERM, handle_signal); { char fips_ipv6[128]; char fips_npub[256]; char listen_port[16]; int have_fips_identity = (lookup_local_fips_identity(fips_ipv6, sizeof(fips_ipv6), fips_npub, sizeof(fips_npub)) == 0); connection_info_clear(); if (listen_mode == NSIGNER_LISTEN_UNIX) { connection_info_add("listen: unix @%s", socket_name); connection_info_add("client: nsigner --socket-name %s client ''", socket_name); printf("System is ready and waiting for connections on @%s.\n", socket_name); } else if (listen_mode == NSIGNER_LISTEN_TCP) { connection_info_add("listen: %s", listen_target); printf("System is ready and waiting for connections on %s.\n", listen_target); } else if (listen_mode == NSIGNER_LISTEN_QREXEC) { connection_info_add("listen: qrexec"); printf("System is ready and waiting for a qrexec request.\n"); } else { connection_info_add("listen: stdio"); printf("System is ready and waiting for a stdio request.\n"); } if (have_fips_identity) { connection_info_add("fips ipv6: %s", fips_ipv6); connection_info_add("fips npub: %s", fips_npub); printf("fips ipv6: %s\n", fips_ipv6); printf("fips npub: %s\n", fips_npub); if (listen_mode == NSIGNER_LISTEN_TCP && extract_listen_port(listen_target, listen_port, sizeof(listen_port)) == 0) { connection_info_add("fips address: http://%s.fips:%s", fips_npub, listen_port); printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port); } } } fflush(stdout); if (listen_mode == NSIGNER_LISTEN_STDIO || listen_mode == NSIGNER_LISTEN_QREXEC) { int hrc = server_handle_one(&server, NULL, NULL); server_stop(&server); crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); return (hrc < 0) ? 1 : 0; } if (listen_mode == NSIGNER_LISTEN_TCP) { pfds[0].fd = server.listen_fd; pfds[0].events = POLLIN; while (g_running && server.running) { int prc = poll(pfds, 1, 200); if (prc < 0) { if (errno == EINTR) { continue; } break; } if (prc > 0 && (pfds[0].revents & POLLIN)) { if (server_handle_one(&server, tcp_activity_stdout_cb, NULL) < 0) { break; } } } server_stop(&server); crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); return 0; } memset(&g_activity_log, 0, sizeof(g_activity_log)); g_auto_approve = 0; server_set_prompt_always_allow(0); { main_tui_context_t main_tui_ctx; main_tui_ctx.role_table = &role_table; main_tui_ctx.mnemonic = &mnemonic; main_tui_ctx.derived_count = &derived_count; main_tui_ctx.socket_name = socket_name; server_set_approval_cb(tui_approval_cb, &main_tui_ctx); tui_install_resize_handler(); render_status(&role_table, &mnemonic, derived_count, socket_name); pfds[0].fd = server.listen_fd; pfds[0].events = POLLIN; pfds[1].fd = STDIN_FILENO; pfds[1].events = POLLIN; while (g_running && server.running) { int prc = poll(pfds, 2, 200); if (prc < 0) { if (errno == EINTR) { continue; } break; } if (tui_resize_pending()) { render_status(&role_table, &mnemonic, derived_count, socket_name); } if (prc > 0 && (pfds[0].revents & POLLIN)) { int hrc = server_handle_one(&server, activity_log_cb, NULL); if (hrc < 0) { break; } render_status(&role_table, &mnemonic, derived_count, socket_name); } if (prc > 0 && (pfds[1].revents & POLLIN)) { char ch = '\0'; if (read(STDIN_FILENO, &ch, 1) > 0) { char lower = (char)tolower((unsigned char)ch); if (lower == 'q' || lower == 'x') { g_running = 0; } else if (lower == 'r') { render_status(&role_table, &mnemonic, derived_count, socket_name); } else if (ch == 'A') { g_auto_approve = g_auto_approve ? 0 : 1; server_set_prompt_always_allow(g_auto_approve); render_status(&role_table, &mnemonic, derived_count, socket_name); } else if (lower == 'l') { mnemonic_source_t relock_source; printf("\n[lock] Session locked. Re-enter mnemonic to unlock.\n"); fflush(stdout); crypto_wipe(&key_store); mnemonic_unload(&mnemonic); relock_source.kind = MNEMONIC_SOURCE_TUI; relock_source.fd = -1; if (g_startup_mnemonic_source_kind != MNEMONIC_SOURCE_TUI) { printf("[lock] Re-unlock uses terminal prompt in this session.\n"); fflush(stdout); } if (load_mnemonic(&mnemonic, &relock_source) != 0) { g_running = 0; } else { derived_count = crypto_derive_all(&key_store, &role_table, &mnemonic); if (derived_count < 0) { g_running = 0; } } render_status(&role_table, &mnemonic, derived_count, socket_name); } } } } server_set_approval_cb(NULL, NULL); } server_stop(&server); crypto_wipe(&key_store); nostr_cleanup(); mnemonic_unload(&mnemonic); printf("Shutdown. All secrets wiped.\n"); return 0; }