#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_PQ_SIG, /* post-quantum signatures (ML-DSA, SLH-DSA) */ PURPOSE_PQ_KEM, /* post-quantum key encapsulation (ML-KEM) */ PURPOSE_UNKNOWN } role_purpose_t; /* Curve enum */ typedef enum { CURVE_SECP256K1 = 0, CURVE_ED25519, CURVE_X25519, CURVE_ML_DSA_65, CURVE_SLH_DSA_128S, CURVE_ML_KEM_768, 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 */ #define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */ /* New algorithm-based verb defines (algorithm is a parameter, not implicit) */ #define VERB_SIGN "sign" #define VERB_VERIFY "verify" #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" #define ENFORCE_ERR_ALGORITHM -4 /* algorithm not valid for verb */ /* New algorithm-based verb defines (algorithm is a parameter, not implicit) */ #define VERB_SIGN "sign" #define VERB_VERIFY "verify" #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" /* 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 #define POLICY_MAX_ALGS 16 #define POLICY_MAX_ALGS 16 /* 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; /* Algorithm-based (new) */ char algorithms[16][32]; /* algorithm names; POLICY_MAX_ALGS */ int alg_count; int index_min; /* -1 = any */ int index_max; /* -1 = any */ /* Common */ 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); /* Check whether caller_id is allowed to invoke `verb` with the given * algorithm and index (algorithm-based policy). Returns POLICY_ALLOW, * POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */ int policy_check_algorithm(const policy_table_t *table, const char *caller_id, const char *verb, const char *algorithm, int index, policy_source_t *out_source); /* Check whether caller_id is allowed to invoke `verb` with the given * algorithm and index (algorithm-based policy). Returns POLICY_ALLOW, * POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH. */ int policy_check_algorithm(const policy_table_t *table, const char *caller_id, const char *verb, const char *algorithm, int index, 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 pq_crypto.h */ /* Algorithm identifiers */ typedef enum { CRYPTO_ALG_SECP256K1 = 0, /* existing, Nostr */ CRYPTO_ALG_ED25519, /* new, SSH signatures */ CRYPTO_ALG_X25519, /* new, key agreement */ CRYPTO_ALG_ML_DSA_65, /* new, PQ signatures */ CRYPTO_ALG_SLH_DSA_128S, /* new, PQ signatures */ CRYPTO_ALG_ML_KEM_768, /* new, PQ KEM */ CRYPTO_ALG_UNKNOWN } crypto_alg_t; /* Key sizes for each algorithm (compile-time constants) */ typedef struct { size_t priv_key_len; size_t pub_key_len; size_t sig_len; /* 0 for KEM */ size_t ciphertext_len; /* 0 for signatures */ size_t shared_secret_len; /* 0 for signatures */ } crypto_alg_sizes_t; /* Get size info for an algorithm. Returns NULL for CRYPTO_ALG_UNKNOWN. */ const crypto_alg_sizes_t *crypto_alg_get_sizes(crypto_alg_t alg); /* Map role_curve_t + role_purpose_t to crypto_alg_t. * Returns CRYPTO_ALG_UNKNOWN for unsupported combinations. */ crypto_alg_t crypto_alg_from_role(role_curve_t curve, role_purpose_t purpose); /* Convert crypto_alg_t to string. Returns NULL for unknown. */ const char *crypto_alg_to_str(crypto_alg_t alg); /* Parse string to crypto_alg_t. Returns CRYPTO_ALG_UNKNOWN for unrecognized. */ crypto_alg_t crypto_alg_from_str(const char *s); /* ed25519: derive keypair from a 32-byte seed. * priv_out and pub_out must be at least 32 bytes each. * Returns 0 on success, -1 on error. */ int crypto_ed25519_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out); /* ed25519: sign a message. priv is 32-byte private key. * sig_out must be at least 64 bytes. Returns 0 on success, -1 on error. */ int crypto_ed25519_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len); /* ed25519: verify a signature. pub is 32-byte public key. * Returns 0 on valid, 1 on invalid, -1 on error. */ int crypto_ed25519_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len); /* x25519: derive keypair from a 32-byte seed. * priv_out and pub_out must be at least 32 bytes each. * Returns 0 on success, -1 on error. */ int crypto_x25519_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out); /* x25519: derive shared secret from our private key and peer's public key. * shared_out must be at least 32 bytes. Returns 0 on success, -1 on error. */ int crypto_x25519_ecdh(const unsigned char *our_priv, size_t priv_len, const unsigned char *peer_pub, size_t pub_len, unsigned char *shared_out, size_t *shared_out_len); /* Derive a 32-byte seed from a mnemonic using a BIP-44 path (SLIP-0010). * seed_out must be at least 32 bytes. Returns 0 on success, -1 on error. */ int crypto_derive_seed_from_mnemonic(const char *mnemonic, const char *path, unsigned char *seed_out, size_t seed_out_len); /* ML-DSA-65: generate keypair from a 32-byte seed (deterministic). * priv_out must be at least 4032 bytes, pub_out at least 1952 bytes. * Returns 0 on success, -1 on error. */ int crypto_ml_dsa_65_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out); /* ML-DSA-65: sign a message. priv is 4032-byte private key. * sig_out must be at least 3309 bytes. Returns 0 on success, -1 on error. */ int crypto_ml_dsa_65_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len); /* ML-DSA-65: verify a signature. pub is 1952-byte public key. * Returns 0 on valid, 1 on invalid, -1 on error. */ int crypto_ml_dsa_65_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len); /* SLH-DSA-128s: generate keypair from a 32-byte seed (deterministic). * priv_out must be at least 64 bytes, pub_out at least 32 bytes. * Returns 0 on success, -1 on error. */ int crypto_slh_dsa_128s_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out); /* SLH-DSA-128s: sign a message. priv is 64-byte private key. * sig_out must be at least 7856 bytes. Returns 0 on success, -1 on error. */ int crypto_slh_dsa_128s_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len); /* SLH-DSA-128s: verify a signature. pub is 32-byte public key. * Returns 0 on valid, 1 on invalid, -1 on error. */ int crypto_slh_dsa_128s_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len); /* ML-KEM-768: generate keypair from a 32-byte seed (deterministic). * priv_out must be at least 2400 bytes, pub_out at least 1184 bytes. * Returns 0 on success, -1 on error. */ int crypto_ml_kem_768_keygen_from_seed(const unsigned char *seed, size_t seed_len, unsigned char *priv_out, unsigned char *pub_out); /* ML-KEM-768: encapsulate. pub is 1184-byte public key. * ct_out must be at least 1088 bytes, ss_out at least 32 bytes. * Returns 0 on success, -1 on error. */ int crypto_ml_kem_768_encaps(const unsigned char *pub, size_t pub_len, unsigned char *ct_out, unsigned char *ss_out); /* ML-KEM-768: decapsulate. priv is 2400-byte secret key, ct is 1088-byte ciphertext. * ss_out must be at least 32 bytes. Returns 0 on success, -1 on error. */ int crypto_ml_kem_768_decaps(const unsigned char *priv, size_t priv_len, const unsigned char *ct, size_t ct_len, unsigned char *ss_out); /* Deterministic PRNG for PQ keygen (replaces PQClean randombytes()). */ void pq_drbg_init(const unsigned char *seed, size_t seed_len); int pq_drbg_randombytes(unsigned char *buf, size_t len); void pq_drbg_zeroize(void); /* from crypto.h */ /* Per-role derived key material (stored in secure memory) */ typedef struct { secure_buf_t private_key; /* mlock'd, variable size per algorithm */ secure_buf_t public_key; /* mlock'd, variable size per algorithm */ char pubkey_hex[8192]; /* hex-encoded public key (PQ pubkeys are large) */ char npub[128]; /* bech32 npub (secp256k1 only, empty for others) */ crypto_alg_t alg; /* which algorithm this key was derived for */ 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 alg_api.h */ /* Check whether a verb is valid for an algorithm (algorithm-based enforcement). * Returns ENFORCE_OK, ENFORCE_ERR_ALGORITHM, or ENFORCE_ERR_UNKNOWN_VERB. * Does NOT check purpose — purpose is irrelevant for the new verbs. */ int enforce_verb_algorithm(const char *verb, crypto_alg_t alg); /* secp256k1 Schnorr (BIP-340) sign arbitrary bytes. * priv is 32-byte scalar, pub is 32-byte x-only pubkey, sig_out is 64 bytes. * Hashes the message with SHA-256 before signing (like Nostr event signing). * Returns 0 on success, -1 on error. */ int crypto_secp256k1_schnorr_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len); /* secp256k1 Schnorr (BIP-340) verify. * pub is 32-byte x-only pubkey, sig is 64 bytes. * Returns 0 on valid, 1 on invalid, -1 on error. */ int crypto_secp256k1_schnorr_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len); /* secp256k1 ECDSA sign arbitrary bytes. * priv is 32-byte scalar, sig_out must be at least 64 bytes (compact DER r||s). * Hashes the message with SHA-256 before signing. * Returns 0 on success, -1 on error. */ int crypto_secp256k1_ecdsa_sign(const unsigned char *priv, size_t priv_len, const unsigned char *msg, size_t msg_len, unsigned char *sig_out, size_t *sig_out_len); /* secp256k1 ECDSA verify. * pub is 32-byte x-only pubkey (converted internally to compressed form). * sig is 64-byte compact (r||s). Returns 0 on valid, 1 on invalid, -1 on error. */ int crypto_secp256k1_ecdsa_verify(const unsigned char *pub, size_t pub_len, const unsigned char *msg, size_t msg_len, const unsigned char *sig, size_t sig_len); /* ---- Algorithm key cache ---- * On-demand key derivation by algorithm+index, separate from the role-based * key_store. Holds up to ALG_KEY_CACHE_MAX derived keys in secure memory. * When full, the oldest entry is evicted (FIFO). */ #define ALG_KEY_CACHE_MAX 32 typedef struct { crypto_alg_t alg; int index; secure_buf_t private_key; secure_buf_t public_key; char pubkey_hex[8192]; char key_id[17]; int valid; } alg_key_entry_t; typedef struct { alg_key_entry_t entries[ALG_KEY_CACHE_MAX]; int count; } algorithm_key_cache_t; /* Initialize an empty cache. */ void alg_key_cache_init(algorithm_key_cache_t *cache); /* Zeroize and free all entries. Idempotent. */ void alg_key_cache_wipe(algorithm_key_cache_t *cache); /* Look up a cached entry by (alg, index). Returns NULL if not present. */ const alg_key_entry_t *alg_key_cache_get(algorithm_key_cache_t *cache, crypto_alg_t alg, int index); /* Derive a key on-demand by (alg, index) and store it in the cache. * Uses the standard derivation path for the algorithm. * Returns 0 on success, -1 on error. */ int alg_key_cache_derive(algorithm_key_cache_t *cache, const mnemonic_state_t *mnemonic, crypto_alg_t alg, int index); /* 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; algorithm_key_cache_t *alg_key_cache; /* algorithm-based on-demand keys */ } 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, algorithm_key_cache_t *alg_key_cache); /* * 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 */ #define INDEX_WHITELIST_MAX 256 #define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) typedef struct { char socket_name[SERVER_SOCKET_NAME_MAX]; /* abstract namespace name (without \0 prefix) */ char last_error[256]; 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; int bridge_source_trusted; int index_whitelist_active; unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; } server_ctx_t; /* Initialize server context. socket_name is the abstract namespace name (e.g. "nsigner"). * 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); /* Mark the unix listener as a trusted bridge socket (qrexec source preamble) */ void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled); /* Set the nostr_index whitelist from a spec string ("all", "1,3,4", "0-3", "0-3,7,9") */ int server_set_index_whitelist(server_ctx_t *ctx, const char *spec); /* Configure non-interactive prompt fallback: -1 disabled, POLICY_ALLOW, or POLICY_DENY */ void server_set_noninteractive_prompt_default(int decision); 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 46 #define NSIGNER_VERSION "v0.0.46" /* 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 NSIGNER_QREXEC_SERVICE_NAME "qubes.NsignerRpc" #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(" [--bridge-source-trusted]"); 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 [--socket-name|--name|-n ] bridge [--to ] qrexec -> unix socket relay", 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"); tui_print(" --bridge-source-trusted Accept qrexec_source preamble on unix connections (bridge mode)"); tui_print(" --allow-index SPEC Restrict which nostr_index values this session can access"); tui_print(" SPEC: 'all' (default), '1,3,4', '0-3', or '0-3,7,9'"); } static int extract_nsigner_socket_from_proc_line(const char *line, 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; } /* * bridge_main — stateless qrexec → unix-socket relay. * * Used as the qubes.NsignerRpc service entrypoint. Reads one framed * JSON-RPC request from stdin (qrexec), forwards it to a persistent * nsigner --listen unix process, and relays one framed response back * to stdout. Before the request, sends a source-qube preamble so the * persistent signer can tag the caller as qubes:. * * The bridge holds no secrets and no mnemonic. It is a dumb pipe. * * Usage: nsigner bridge [--to ] * --to abstract unix socket name (default: nsigner) * * Environment: * QREXEC_REMOTE_DOMAIN set by qrexec to the source qube name */ static int bridge_main(int argc, char *argv[], const char *socket_name, int socket_name_explicit) { const char *target_socket = socket_name; char discovered[SERVER_SOCKET_NAME_MAX]; int fd; char *request = NULL; char *response = NULL; const char *source_qube; char preamble[256]; int i; /* Parse bridge-specific args */ for (i = 0; i < argc; i++) { if (strcmp(argv[i], "--to") == 0) { if (i + 1 >= argc) { fprintf(stderr, "bridge: --to requires a socket name\n"); return 1; } target_socket = argv[i + 1]; socket_name_explicit = 1; i++; } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { fprintf(stderr, "Usage: nsigner bridge [--to ]\n"); return 0; } else { fprintf(stderr, "bridge: unknown argument: %s\n", argv[i]); return 1; } } if (target_socket == NULL || target_socket[0] == '\0') { target_socket = "nsigner"; } if (!socket_name_explicit) { if (discover_single_socket_name(discovered, sizeof(discovered)) == 0) { target_socket = discovered; } } /* Read the source qube from the qrexec environment */ source_qube = getenv("QREXEC_REMOTE_DOMAIN"); if (source_qube == NULL || source_qube[0] == '\0') { /* Not running under qrexec — relay anyway but with no source tag. * The persistent signer will identify the caller by uid only. */ source_qube = ""; } /* Connect to the persistent signer's abstract unix socket */ fd = connect_abstract_socket(target_socket); if (fd < 0) { fprintf(stderr, "bridge: cannot connect to @%s: %s\n", target_socket, strerror(errno)); return 1; } /* Send the source-qube preamble (framed JSON) */ { int plen; plen = snprintf(preamble, sizeof(preamble), "{\"qrexec_source\":\"%s\"}", source_qube); if (plen < 0 || (size_t)plen >= sizeof(preamble)) { fprintf(stderr, "bridge: source qube name too long\n"); close(fd); return 1; } if (transport_send_framed(fd, preamble) != 0) { fprintf(stderr, "bridge: failed to send preamble: %s\n", strerror(errno)); close(fd); return 1; } } /* Read one framed request from stdin (qrexec) and forward to socket */ if (transport_recv_framed(STDIN_FILENO, &request, SERVER_MAX_MSG_SIZE) != 0) { fprintf(stderr, "bridge: failed to read request from stdin: %s\n", strerror(errno)); close(fd); return 1; } if (transport_send_framed(fd, request) != 0) { fprintf(stderr, "bridge: failed to forward request: %s\n", strerror(errno)); free(request); close(fd); return 1; } free(request); /* Read one framed response from socket and relay to stdout (qrexec) */ if (transport_recv_framed(fd, &response, SERVER_MAX_MSG_SIZE) != 0) { fprintf(stderr, "bridge: failed to read response from signer: %s\n", strerror(errno)); close(fd); return 1; } if (transport_send_framed(STDOUT_FILENO, response) != 0) { fprintf(stderr, "bridge: failed to relay response: %s\n", strerror(errno)); free(response); close(fd); return 1; } 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[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'; } 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++; } } } /* Transport selection bitmask for interactive menu */ #define TRANSPORT_UNIX 0x01 #define TRANSPORT_QREXEC_BRIDGE 0x02 #define TRANSPORT_TCP 0x04 #define TRANSPORT_QREXEC_ONESHOT 0x08 /* * Interactive transport selection menu. * Shown after mnemonic entry when no --listen flag was given and stdin is a TTY. * Returns a bitmask of selected transports, or 0 on error/no selection. */ static int prompt_transport_selection(void) { int selected = TRANSPORT_UNIX; /* default: local unix socket */ char input[32]; for (;;) { tui_render_content_screen(NULL, "Transport — how should other programs reach this signer?"); printf("Select one or more (type a number to toggle, 'a' for all, Enter to confirm):\n\n"); printf(" [%s] 1. Local Unix socket (same machine/qube)\n", (selected & TRANSPORT_UNIX) ? "x" : " "); printf(" [%s] 2. Qubes qrexec bridge (other qubes via qrexec, no network)\n", (selected & TRANSPORT_QREXEC_BRIDGE) ? "x" : " "); printf(" [%s] 3. TCP listener (FIPS mesh or local network)\n", (selected & TRANSPORT_TCP) ? "x" : " "); printf("\n [a] select all Enter = confirm\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { return 0; } /* Trim trailing whitespace */ { size_t len = strlen(input); while (len > 0 && (input[len-1] == '\n' || input[len-1] == '\r' || input[len-1] == ' ' || input[len-1] == '\t')) { input[--len] = '\0'; } } if (input[0] == '\0') { /* Enter pressed — confirm current selection */ if (selected == 0) { printf("At least one transport must be selected.\n"); continue; } break; } if (input[0] == 'a' || input[0] == 'A') { selected = TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE | TRANSPORT_TCP; continue; } if (input[0] == '1') { selected ^= TRANSPORT_UNIX; } else if (input[0] == '2') { selected ^= TRANSPORT_QREXEC_BRIDGE; } else if (input[0] == '3') { selected ^= TRANSPORT_TCP; } else { printf("Invalid input. Type 1-3, 'a', or Enter to confirm.\n"); } } return selected; } /* * Interactive index whitelist prompt. * Shown after transport selection when no --allow-index flag was given. * Returns a malloc'd spec string (caller must free), or NULL for "all". */ static char *prompt_index_whitelist(void) { char input[256]; for (;;) { tui_render_content_screen(NULL, "Index whitelist — restrict which nostr_index values this session can access"); printf("Enter allowed indices, or press Enter for 'all' (no restriction):\n\n"); printf(" Examples:\n"); printf(" all (default — allow all indices)\n"); printf(" 0 (only index 0)\n"); printf(" 0,1,3 (specific indices)\n"); printf(" 0-3 (range 0 through 3)\n"); printf(" 0,2-3,7 (mixed list and ranges)\n"); printf("\n Enter = all\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { return NULL; } /* Trim trailing whitespace */ { size_t len = strlen(input); while (len > 0 && (input[len-1] == '\n' || input[len-1] == '\r' || input[len-1] == ' ' || input[len-1] == '\t')) { input[--len] = '\0'; } } if (input[0] == '\0') { /* Enter pressed — default to "all" */ return NULL; } /* Validate by parsing into a temp whitelist */ { server_ctx_t tmp; memset(&tmp, 0, sizeof(tmp)); if (server_set_index_whitelist(&tmp, input) != 0) { printf("Invalid spec: '%s'. Try again or press Enter for 'all'.\n", input); continue; } } return strdup(input); } } 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; static algorithm_key_cache_t alg_key_cache; struct pollfd pfds[3]; /* max: 2 listeners (unix+tcp) + stdin */ 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; int listen_mode_explicit = 0; 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; int bridge_source_trusted = 0; const char *allow_index_spec = NULL; 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; } listen_mode_explicit = 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; } if (strcmp(argv[argi], "--bridge-source-trusted") == 0) { bridge_source_trusted = 1; argi += 1; continue; } if (strcmp(argv[argi], "--allow-index") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } allow_index_spec = argv[argi + 1]; argi += 2; continue; } break; } 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], "bridge") == 0) { /* bridge: stateless qrexec → unix-socket relay (no mnemonic, no listen) */ return bridge_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)); alg_key_cache_init(&alg_key_cache); 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; /* Role-based entries: register the nostr_index role if needed. * Algorithm-based entries (alg_count > 0) don't need a role. */ if (entry.alg_count == 0 && 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; } if (entry.alg_count > 0) { fprintf(stderr, "[PREAPPROVE] caller=%s algorithm=%s verbs=%d\n", entry.caller, entry.algorithms[0], entry.verb_count); } else { 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, &alg_key_cache); /* * Interactive transport selection: if no --listen flag was given and * stdin is a TTY, show the multi-select transport menu. This lets the * user choose how other programs reach the signer without memorizing * CLI flags. CLI flags skip the menu (backward compatible). */ int transport_mask = 0; int multi_listen = 0; /* set when multiple persistent listeners are active */ server_ctx_t servers[2]; /* max: unix + tcp */ int server_count = 0; int unix_server_idx = -1; int tcp_server_idx = -1; if (!listen_mode_explicit && isatty(STDIN_FILENO) && mnemonic_source.kind == MNEMONIC_SOURCE_TUI) { transport_mask = prompt_transport_selection(); if (transport_mask == 0) { fprintf(stderr, "No transport selected. Exiting.\n"); crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } /* Index whitelist prompt (only if --allow-index wasn't given on CLI) */ if (allow_index_spec == NULL) { char *wl_spec = prompt_index_whitelist(); if (wl_spec != NULL) { allow_index_spec = wl_spec; /* will be freed at program exit */ } } /* Persistent listeners — configure from menu selection */ multi_listen = 1; /* Unix socket (with or without bridge-source-trusted) */ if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) { /* Use fixed name "nsigner" for scripted access */ socket_name = "nsigner"; socket_name_explicit = 1; listen_mode = NSIGNER_LISTEN_UNIX; listen_target = socket_name; if (transport_mask & TRANSPORT_QREXEC_BRIDGE) { bridge_source_trusted = 1; } } /* TCP listener */ if (transport_mask & TRANSPORT_TCP) { /* Will be started as a second listener */ } } 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); alg_key_cache_wipe(&alg_key_cache); 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); alg_key_cache_wipe(&alg_key_cache); 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); alg_key_cache_wipe(&alg_key_cache); 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 (bridge_source_trusted) { server_set_bridge_source_trusted(&server, 1); } if (allow_index_spec != NULL) { if (server_set_index_whitelist(&server, allow_index_spec) != 0) { fprintf(stderr, "Invalid --allow-index spec: %s\n", allow_index_spec); fprintf(stderr, "Expected: 'all', '1,3,4', '0-3', or '0-3,7,9'\n"); crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } } if (server_start(&server) != 0) { if (listen_mode == NSIGNER_LISTEN_UNIX) { fprintf(stderr, "Failed to start server on @%s: %s\n", socket_name, server_last_error(&server)); } 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); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } /* * Multi-listener: if interactive menu selected TCP in addition to unix, * start a second server context for TCP. The primary server (unix) is * already running. Both share the same dispatcher and policy. */ if (multi_listen && (transport_mask & TRANSPORT_TCP)) { const char *tcp_target = "tcp:[::]:8080"; tcp_server_idx = server_count; server_init(&servers[tcp_server_idx], tcp_target, 0, /* socket_name_explicit = 0 for TCP */ NSIGNER_LISTEN_TCP, NSIGNER_AUTH_REQUIRED, auth_skew_seconds, &dispatcher, &policy); if (allow_index_spec != NULL) { server_set_index_whitelist(&servers[tcp_server_idx], allow_index_spec); } if (server_start(&servers[tcp_server_idx]) != 0) { fprintf(stderr, "Failed to start TCP server on %s: %s\n", tcp_target, server_last_error(&servers[tcp_server_idx])); server_stop(&server); crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } server_count++; } /* Copy the primary (unix) server into the array for unified polling */ if (multi_listen) { unix_server_idx = server_count; servers[unix_server_idx] = server; /* struct copy */ server_count++; } (void)signal(SIGINT, handle_signal); (void)signal(SIGTERM, handle_signal); /* * Ignore SIGPIPE so that writing to a socket whose peer has closed returns * EPIPE instead of killing the signer process. A long-running attended * signer must survive clients that disconnect abruptly (e.g. the * nostr_core_lib client reconnects per request, so it closes the previous * connection; if the server happens to write while the peer is closing, * SIGPIPE would otherwise terminate the whole signer). */ (void)signal(SIGPIPE, SIG_IGN); { 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 (multi_listen) { /* Multi-listener mode: show all active transports */ if (transport_mask & (TRANSPORT_UNIX | TRANSPORT_QREXEC_BRIDGE)) { if (bridge_source_trusted) { connection_info_add("listen: unix @%s (bridge-source-trusted)", socket_name); } else { connection_info_add("listen: unix @%s", socket_name); } connection_info_add("client: nsigner --socket-name %s client ''", socket_name); if (transport_mask & TRANSPORT_QREXEC_BRIDGE) { connection_info_add("qrexec service: %s", NSIGNER_QREXEC_SERVICE_NAME); connection_info_add("qrexec caller: qrexec-client-vm %s", NSIGNER_QREXEC_SERVICE_NAME); printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME); } printf("System is ready and waiting for connections on @%s.\n", socket_name); } if (transport_mask & TRANSPORT_TCP) { connection_info_add("listen: tcp [::]:8080"); printf("System is ready and waiting for connections on tcp:[::]:8080.\n"); } } else 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); if (bridge_source_trusted) { connection_info_add("qrexec service: %s", NSIGNER_QREXEC_SERVICE_NAME); connection_info_add("qrexec caller: qrexec-client-vm %s", NSIGNER_QREXEC_SERVICE_NAME); printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_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); alg_key_cache_wipe(&alg_key_cache); 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); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 0; } memset(&g_activity_log, 0, sizeof(g_activity_log)); g_auto_approve = allow_all ? 1 : 0; server_set_prompt_always_allow(g_auto_approve); { 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); /* Set up poll fds: listener(s) + stdin */ int npfds = 0; if (multi_listen) { for (int si = 0; si < server_count; si++) { pfds[npfds].fd = servers[si].listen_fd; pfds[npfds].events = POLLIN; npfds++; } } else { pfds[npfds].fd = server.listen_fd; pfds[npfds].events = POLLIN; npfds++; } pfds[npfds].fd = STDIN_FILENO; pfds[npfds].events = POLLIN; npfds++; int stdin_pfd_idx = npfds - 1; while (g_running && server.running) { int prc = poll(pfds, npfds, 200); if (prc < 0) { if (errno == EINTR) { continue; } break; } if (tui_resize_pending()) { render_status(&role_table, &mnemonic, derived_count, socket_name); } /* Check all listener fds for activity */ if (prc > 0) { if (multi_listen) { for (int si = 0; si < server_count; si++) { if (pfds[si].revents & POLLIN) { int hrc = server_handle_one(&servers[si], activity_log_cb, NULL); if (hrc < 0) { g_running = 0; break; } render_status(&role_table, &mnemonic, derived_count, socket_name); } } } else { if (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[stdin_pfd_idx].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); alg_key_cache_wipe(&alg_key_cache); 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); } if (multi_listen) { for (int si = 0; si < server_count; si++) { server_stop(&servers[si]); } } else { server_stop(&server); } crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); printf("Shutdown. All secrets wiped.\n"); return 0; }