#define _GNU_SOURCE /* NSIGNER_HEADERLESS_DECLS_BEGIN */ #include #include #include #include #include "otp_pad.h" /* from secure_mem.h */ /* * Secure memory buffer — mlock'd, zeroized on free. * Used for mnemonic phrases, private keys, and any sensitive material. */ typedef struct { void *data; /* pointer to locked allocation */ size_t size; /* usable size in bytes */ int locked; /* 1 if mlock succeeded */ } secure_buf_t; /* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */ int secure_buf_alloc(secure_buf_t *buf, size_t size); /* Zeroize and free a secure buffer. Always succeeds (idempotent). */ void secure_buf_free(secure_buf_t *buf); /* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */ void secure_memzero(void *ptr, size_t len); /* Allow secure_buf_alloc to succeed even when mlock fails (development only). */ void secure_buf_allow_unlocked(void); /* 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 */ int path_range_lo; /* for SELECTOR_ROLE_PATH: inclusive lower bound for %d; -1 = fixed path */ int path_range_hi; /* inclusive upper bound; == path_range_lo for single */ int path_default_index; /* default index when client sends {"role":...} without "index"; -1 = require explicit */ int path_allowed_indices[64]; /* explicit set of allowed indices (for sets); 0 = use range */ int path_allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */ int requires_approval; /* 0 = role-as-password (no prompt), 1 = require interactive approval */ } 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); /* Register a SELECTOR_ROLE_PATH role bound to an explicit derivation path template. */ int role_table_register_role_path(role_table_t *table, const char *name, const char *path, role_purpose_t purpose, role_curve_t curve, int range_lo, int range_hi, int default_index, const int *allowed_indices, int allowed_count); /* * Check whether a concrete derivation path matches a role's path template. * The template may contain a "%d" placeholder (with optional "'" hardened marker). * Returns 1 if the path matches the template, 0 if not. */ int role_path_matches_template(const char *path, const char *template); /* 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 */ #define SELECTOR_ERR_PATH_MISMATCH -4 /* role_path doesn't match role's template */ #define SELECTOR_ERR_NOSTR_INDEX_DEPRECATED -5 /* nostr_index is deprecated */ #define SELECTOR_ERR_INDEX_DEPRECATED -6 /* index is deprecated for nostr verbs */ #define SELECTOR_ERR_ROLE_REQUIRED -7 /* --role is required when using --path */ #define SELECTOR_ERR_PATH_REQUIRED -8 /* --path is required for roles with variable path templates */ /* 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]; int has_index; /* 1 if "index" field was present (for named path-roles) */ int index; /* index value for named path-role template */ } 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 VERB_DERIVE "derive" #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 VERB_DERIVE "derive" /* Known verbs */ #define VERB_GET_PUBLIC_KEY "get_public_key" #define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" #define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" #define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" #define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" #define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" #define VERB_NOSTR_NIP04_DECRYPT "nostr_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 (nostr_sign_event, nostr_get_public_key, nostr_nip44_*, nostr_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 16777216 #define NSIGNER_LISTEN_UNIX 0 #define NSIGNER_LISTEN_STDIO 1 #define NSIGNER_LISTEN_QREXEC 2 #define NSIGNER_LISTEN_TCP 3 #define NSIGNER_LISTEN_HTTP 4 #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) #define PATH_WHITELIST_MAX_TEMPLATES 16 #define PATH_TEMPLATE_MAX_LEN 128 #define PATH_TEMPLATE_MAX_INDICES 64 /* max allowed indices per template (for sets) */ typedef struct { char template[PATH_TEMPLATE_MAX_LEN]; /* e.g. "m/44'/1237'/%d/1/0" — one %d placeholder */ int range_lo; /* inclusive lower bound (for range form) */ int range_hi; /* inclusive upper bound (== range_lo for single) */ int allowed_indices[PATH_TEMPLATE_MAX_INDICES]; /* explicit set of allowed indices */ int allowed_count; /* 0 = use range_lo/range_hi; >0 = use allowed_indices */ } path_template_t; typedef struct { int active; /* 1 if any path templates are configured */ int count; path_template_t templates[PATH_WHITELIST_MAX_TEMPLATES]; } path_whitelist_t; 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]; path_whitelist_t path_whitelist; /* path-template whitelist for role_path requests */ } 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); /* Set the unified whitelist (integer nostr_index + path templates) from a spec string. * Spec: "all", or comma-separated tokens. Integer tokens ("0-3","1,3,4") set the * nostr_index bitmap. Path-template tokens ("m/44'/1237'/0-3/1/0") set the path whitelist. */ int server_set_path_whitelist(server_ctx_t *ctx, const char *spec); /* Check if a role_path is allowed by the path whitelist. Returns 1 if allowed, 0 if not. */ int server_path_whitelist_allows(const server_ctx_t *ctx, const char *role_path); /* 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 1 #define NSIGNER_VERSION_PATCH 26 #define NSIGNER_VERSION "v0.1.26" /* 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 #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 #define CONNECTION_INFO_STR_MAX 256 typedef struct { char lines[ACTIVITY_LOG_CAP][256]; int count; } activity_log_t; /* Structured connection info for the on-demand connection display. * Each entry represents one transport block: a title (e.g. "Unix socket"), * a connection string (the address/endpoint), an optional example command, * and an optional extra line (e.g. qrexec bridge info under Unix). */ typedef struct { char title[64]; char connection_string[CONNECTION_INFO_STR_MAX]; char example[CONNECTION_INFO_STR_MAX]; /* empty if none */ char extra[CONNECTION_INFO_STR_MAX]; /* empty if none */ } connection_info_entry_t; static volatile sig_atomic_t g_running = 1; static activity_log_t g_activity_log; 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 connection_info_entry_t g_connection_info[CONNECTION_INFO_CAP]; static int g_connection_info_count = 0; /* OTP pad status line (shown at bottom of connection display) */ static char g_otp_pad_status[CONNECTION_INFO_STR_MAX]; static const TuiMenuItem g_main_menu_items[] = { {"^_l^: lock/reunlock", 'l'}, {"^_r^: refresh", 'r'}, {"^_d^: display connections", 'd'}, {"^_q^:/x quit", 'q'} }; 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 render_connections(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)); g_otp_pad_status[0] = '\0'; } /* Add a transport block to the connection info. * title: transport name (e.g. "Unix socket", "FIPS", "HTTP") * connection_str: the address/endpoint clients use to reach the signer * example: example client command (empty string "" if none) * extra: optional extra line under the block (e.g. qrexec info; "" if none) */ static void connection_info_add_transport(const char *title, const char *connection_str, const char *example, const char *extra) { connection_info_entry_t *e; if (g_connection_info_count >= CONNECTION_INFO_CAP) { return; } e = &g_connection_info[g_connection_info_count]; memset(e, 0, sizeof(*e)); if (title) { strncpy(e->title, title, sizeof(e->title) - 1); } if (connection_str) { strncpy(e->connection_string, connection_str, sizeof(e->connection_string) - 1); } if (example) { strncpy(e->example, example, sizeof(e->example) - 1); } if (extra) { strncpy(e->extra, extra, sizeof(e->extra) - 1); } g_connection_info_count++; } /* Set the OTP pad status line (shown at bottom of connection display). */ static void connection_info_set_otp(const char *fmt, ...) { va_list ap; if (fmt == NULL) { g_otp_pad_status[0] = '\0'; return; } va_start(ap, fmt); (void)vsnprintf(g_otp_pad_status, sizeof(g_otp_pad_status), fmt, ap); va_end(ap); } 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; } /* * Read a line from stdin with inline editing support, using termios raw mode. * Pre-fills the buffer with `prefill` (if non-NULL), positions the cursor at * the end, and allows arrow-key navigation, backspace, delete, home, end, * and regular character insertion. On Enter, returns the edited string in * `buf`. Returns 0 on success, -1 on error/EOF. * * Only works when stdin is a TTY. Falls back to read_line_stdin if not a TTY * (in which case prefill is ignored). * * `prompt` is the label text printed before the editable field (e.g. * " Role name [main]: "). It is reprinted on every redraw so that the * prompt does not disappear when the user starts editing. */ static int read_line_editable(char *buf, size_t buf_sz, const char *prefill, const char *prompt) { struct termios old_term, new_term; size_t len = 0; /* current text length */ size_t pos = 0; /* cursor position (0..len) */ size_t prompt_len = 0;/* length of prompt string (for cursor math) */ int fd = STDIN_FILENO; int was_raw = 0; if (buf == NULL || buf_sz == 0) { return -1; } if (prompt != NULL) { prompt_len = strlen(prompt); } /* If not a TTY, fall back to plain fgets */ if (!isatty(fd)) { return read_line_stdin(buf, buf_sz); } /* Pre-fill */ if (prefill != NULL) { len = strlen(prefill); if (len >= buf_sz) len = buf_sz - 1; memcpy(buf, prefill, len); buf[len] = '\0'; pos = len; } else { buf[0] = '\0'; } /* Enter raw mode */ if (tcgetattr(fd, &old_term) == 0) { new_term = old_term; new_term.c_lflag &= ~(ICANON | ECHO); new_term.c_cc[VMIN] = 1; new_term.c_cc[VTIME] = 0; if (tcsetattr(fd, TCSANOW, &new_term) == 0) { was_raw = 1; } } /* Draw the initial prompt + pre-filled text */ if (prompt != NULL) { fputs(prompt, stdout); } if (len > 0) { fputs(buf, stdout); } fflush(stdout); for (;;) { char ch; ssize_t n = read(fd, &ch, 1); if (n <= 0) { if (was_raw) tcsetattr(fd, TCSANOW, &old_term); return -1; } if (ch == '\n' || ch == '\r') { /* Enter — done */ buf[len] = '\0'; fputc('\n', stdout); fflush(stdout); break; } else if (ch == 0x7f || ch == 0x08) { /* Backspace (DEL or BS) — delete char before cursor */ if (pos > 0) { size_t i; for (i = pos - 1; i < len - 1; i++) { buf[i] = buf[i + 1]; } len--; pos--; buf[len] = '\0'; /* Redraw: move to start of field, clear line, redraw, reposition */ fputs("\r\033[K", stdout); /* CR + clear to end of line */ if (prompt != NULL) fputs(prompt, stdout); fputs(buf, stdout); if (pos < len) { /* Move cursor left to pos */ printf("\033[%zuD", len - pos); } fflush(stdout); } } else if (ch == 0x1b) { /* Escape sequence — arrow keys, etc. */ char seq[2]; if (read(fd, &seq[0], 1) <= 0) continue; if (read(fd, &seq[1], 1) <= 0) continue; if (seq[0] == '[') { if (seq[1] == 'D') { /* Left arrow */ if (pos > 0) { pos--; fputs("\033[D", stdout); fflush(stdout); } } else if (seq[1] == 'C') { /* Right arrow */ if (pos < len) { pos++; fputs("\033[C", stdout); fflush(stdout); } } else if (seq[1] == 'A' || seq[1] == 'B') { /* Up/Down — ignore */ } else if (seq[1] == 'H') { /* Home — move to start */ if (pos > 0) { printf("\033[%zuD", pos); pos = 0; fflush(stdout); } } else if (seq[1] == 'F') { /* End — move to end */ if (pos < len) { printf("\033[%zuC", len - pos); pos = len; fflush(stdout); } } else if (seq[1] == '3') { /* Delete (Delete key = ESC [ 3 ~ ) */ char tilde; if (read(fd, &tilde, 1) <= 0) continue; if (tilde == '~' && pos < len) { size_t i; for (i = pos; i < len - 1; i++) { buf[i] = buf[i + 1]; } len--; buf[len] = '\0'; fputs("\r\033[K", stdout); if (prompt != NULL) fputs(prompt, stdout); fputs(buf, stdout); if (pos < len) { printf("\033[%zuD", len - pos); } fflush(stdout); } } } } else if (ch == 0x01) { /* Ctrl-A — home */ if (pos > 0) { printf("\033[%zuD", pos); pos = 0; fflush(stdout); } } else if (ch == 0x05) { /* Ctrl-E — end */ if (pos < len) { printf("\033[%zuC", len - pos); pos = len; fflush(stdout); } } else if (ch == 0x15) { /* Ctrl-U — clear entire line */ if (pos > 0) { fputs("\r\033[K", stdout); if (prompt != NULL) fputs(prompt, stdout); len = 0; pos = 0; buf[0] = '\0'; fflush(stdout); } } else if ((unsigned char)ch >= 0x20 && (unsigned char)ch < 0x7f) { /* Regular printable character — insert at cursor */ if (len < buf_sz - 1) { size_t i; /* Shift characters right to make room */ for (i = len; i > pos; i--) { buf[i] = buf[i - 1]; } buf[pos] = ch; len++; buf[len] = '\0'; /* Redraw from cursor position */ fputs("\r\033[K", stdout); if (prompt != NULL) fputs(prompt, stdout); fputs(buf, stdout); pos++; if (pos < len) { printf("\033[%zuD", len - pos); } fflush(stdout); } } /* Ignore other control characters */ } /* Restore terminal */ if (was_raw) { tcsetattr(fd, TCSANOW, &old_term); } 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 ]... [--register-role ]..."); tui_print(" [--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|http: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(" --register-role SPEC"); tui_print(" Register a named path-role non-interactively (repeatable)"); tui_print(" SPEC: ::"); tui_print(" e.g. nostr_range:secp256k1:m/44'/1237'/*'/0/0"); tui_print(" Suppresses the default 'main' role in non-TTY mode"); 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(" --otp-pad-dir DIR Bind an OTP pad directory at startup (one pad per session)"); tui_print(" --otp-pad SPEC Pad chksum (64 hex) or unique prefix; required with --otp-pad-dir"); tui_print(" --otp-allow-blkback Allow pads on qvm-block (blkback) devices; NOT for production pads"); } 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_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) { printf("%s\n", name_no_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 = NULL; if (argc < 1) { fprintf(stderr, "Usage: nsigner [--socket-name|--name|-n ] client '' | -\n"); return 1; } if (strcmp(argv[0], "-") == 0) { /* Heap-allocate the stdin buffer (SERVER_MAX_MSG_SIZE is 16MB — too * large for the stack). */ stdin_buf = (char *)malloc(SERVER_MAX_MSG_SIZE + 1); if (!stdin_buf) { fprintf(stderr, "Failed to allocate stdin buffer\n"); return 1; } if (read_line_stdin(stdin_buf, SERVER_MAX_MSG_SIZE + 1) != 0) { fprintf(stderr, "Failed to read request from stdin\n"); free(stdin_buf); 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); free(stdin_buf); return 1; } if (transport_recv_framed(fd, &response, SERVER_MAX_MSG_SIZE) != 0) { perror("recv"); close(fd); free(stdin_buf); return 1; } printf("%s\n", response); free(response); close(fd); free(stdin_buf); 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: if (r->selector_type == SELECTOR_NOSTR_INDEX) { (void)snprintf(out, out_size, "m/44'/1237'/%d'/0/0", r->nostr_index); } else if (r->path_range_lo < 0 && r->path_allowed_count == 0) { /* Fixed path (no %d placeholder) */ (void)snprintf(out, out_size, "%s", r->role_path); } else { /* Template path — replace %d with range or set description */ char range_str[64]; char display[ROLE_PATH_MAX + 64]; const char *pct; const char *tail; if (r->path_allowed_count > 0) { /* Set: e.g. "1+34+54" */ int si; int off = 0; for (si = 0; si < r->path_allowed_count && off < (int)sizeof(range_str) - 12; ++si) { off += snprintf(range_str + off, sizeof(range_str) - off, "%s%d", (si == 0) ? "" : "+", r->path_allowed_indices[si]); } range_str[off] = '\0'; } else if (r->path_range_lo == r->path_range_hi) { /* Single index */ snprintf(range_str, sizeof(range_str), "%d", r->path_range_lo); } else { /* Range */ snprintf(range_str, sizeof(range_str), "%d-%d", r->path_range_lo, r->path_range_hi); } /* Replace first %d in role_path with range_str */ pct = strstr(r->role_path, "%d"); if (pct != NULL) { size_t prefix_len = (size_t)(pct - r->role_path); tail = pct + 2; /* skip "%d" */ snprintf(display, sizeof(display), "%.*s%s%s", (int)prefix_len, r->role_path, range_str, tail); } else { snprintf(display, sizeof(display), "%s", r->role_path); } (void)snprintf(out, out_size, "%s", display); } break; default: out[0] = '\0'; break; } } /* On-demand connection instructions display (press 'd'). * Shows each transport as a titled block with the connection string * and an example client command, with blank lines between transports. */ static void render_connections(const role_table_t *role_table, const mnemonic_state_t *mnemonic, int derived_count, const char *socket_name) { TuiFrame frame = { "n_signer", NSIGNER_VERSION, "> Connection Instructions" }; TuiSize size = tui_terminal_size(); char status_buf[256]; int i; /* Use a full screen clear (not tui_clear_continuous) because the * connections display is a modal view that may exceed the terminal * height. tui_clear_continuous assumes the content fits within * term_height lines; if it doesn't, the extra lines scroll past and * the cursor-up can't reach them. A full clear + home cursor works * regardless of content height. */ printf("\033[2J\033[H"); fflush(stdout); tui_render_top_frame(&frame, size.width); tui_print(""); for (i = 0; i < g_connection_info_count; i++) { connection_info_entry_t *e = &g_connection_info[i]; /* Transport title */ tui_print("^*%s^:", e->title); tui_print(""); /* Connection string (indented) */ tui_print(" %s", e->connection_string); tui_print(""); /* Example command (if present) */ if (e->example[0] != '\0') { tui_print(" Example:"); /* The example may contain embedded newlines for multi-line commands */ { const char *p = e->example; const char *nl; while (*p != '\0') { nl = strchr(p, '\n'); if (nl != NULL) { tui_print(" %.*s", (int)(nl - p), p); p = nl + 1; } else { tui_print(" %s", p); break; } } } tui_print(""); } /* Extra info (if present, e.g. qrexec bridge) */ if (e->extra[0] != '\0') { const char *p = e->extra; const char *nl; while (*p != '\0') { nl = strchr(p, '\n'); if (nl != NULL) { tui_print(" %.*s", (int)(nl - p), p); p = nl + 1; } else { tui_print(" %s", p); break; } } tui_print(""); } tui_print(""); } /* OTP pad status */ if (g_otp_pad_status[0] != '\0') { tui_print("%s", g_otp_pad_status); tui_print(""); } /* Status line */ (void)snprintf(status_buf, sizeof(status_buf), "session=%s (%d words) signer=%s derived=%d", mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked", (mnemonic != NULL) ? mnemonic->word_count : 0, (socket_name != NULL) ? socket_name : "(none)", derived_count); tui_print("%s", status_buf); tui_print(""); tui_print("Press any key to return"); tui_anchor_prompt(0, 0); fflush(stdout); } 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(); const int left_col = 0; char status_buf[256]; tui_clear_continuous(size.height); tui_render_top_frame(&frame, size.width); 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}, {"Derivation path", 24, 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_print(""); (void)snprintf(status_buf, sizeof(status_buf), "session=%s (%d words) signer=%s derived=%d", mnemonic_is_loaded(mnemonic) ? "unlocked" : "locked", (mnemonic != NULL) ? mnemonic->word_count : 0, (socket_name != NULL) ? socket_name : "(none)", derived_count); tui_print("%s", status_buf); tui_print(""); tui_render_menu(&menu, left_col); 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; } /* * Interactive role preset menu wizard. * Replaces the old blank-template prompt with a preset menu. * Returns 0 on success (at least one role created), -1 if no roles created or error. */ /* Forward declarations for helper functions used by prompt_named_path_roles */ static int parse_path_template_for_role(const char *token, char *template_out, size_t template_sz, int *range_lo, int *range_hi, int *allowed_indices_out, int max_allowed, int *allowed_count_out); static role_purpose_t purpose_from_path(const char *path); /* When the role wizard binds an OTP pad, the pad dir/spec are stored here so * the later interactive OTP pad prompt (in main()) can be skipped. */ static char g_wizard_otp_dir[256]; static char g_wizard_otp_spec[256]; static int prompt_named_path_roles(role_table_t *role_table) { char input[256]; int roles_created = 0; if (role_table == NULL) { return -1; } for (;;) { tui_render_content_screen(NULL, "Define a role — bind a role name to a derivation path template"); printf("Define a role:\n"); printf(" 1. Standard Nostr (NIP-06): secp256k1, m/44'/1237'/0'/0/0\n"); printf(" 2. Standard Nostr range: secp256k1, m/44'/1237'/*'/0/0\n"); printf(" 3. Nostr agent range (hardened): secp256k1, m/44'/1237'/*'/1'/0'\n"); printf(" 4. SSH role: ed25519, m/44'/102001'/0'/0'/0'\n"); printf(" 5. Age/x25519 role: x25519, m/44'/102002'/0'/0'/0'\n"); printf(" 6. ML-DSA-65 role: post-quantum signatures, m/44'/102003'/0'/0'/0'\n"); printf(" 7. SLH-DSA-128s role: post-quantum signatures, m/44'/102004'/0'/0'/0'\n"); printf(" 8. ML-KEM-768 role: post-quantum KEM, m/44'/102005'/0'/0'/0'\n"); printf(" 9. OTP role (one-time pad encryption)\n"); printf(" 10. Custom path\n"); printf(" Select [1]: "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { return (roles_created > 0) ? 0 : -1; } { 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'; } } int choice = 1; if (input[0] != '\0') { choice = atoi(input); if (choice < 1 || choice > 10) choice = 1; } /* Preset data */ const char *default_name = "main"; const char *default_path = "m/44'/1237'/0'/0/0"; const char *default_curve_str = "secp256k1"; role_purpose_t default_purpose = PURPOSE_NOSTR; switch (choice) { case 2: default_name = "nostr_range"; default_path = "m/44'/1237'/*'/0/0"; break; case 3: default_name = "nostr_agent"; default_path = "m/44'/1237'/*'/1'/0'"; break; case 4: default_name = "ssh"; default_path = "m/44'/102001'/0'/0'/0'"; default_curve_str = "ed25519"; default_purpose = PURPOSE_SSH; break; case 5: default_name = "age"; default_path = "m/44'/102002'/0'/0'/0'"; default_curve_str = "x25519"; default_purpose = PURPOSE_AGE; break; case 6: default_name = "ml_dsa_65"; default_path = "m/44'/102003'/0'/0'/0'"; default_curve_str = "ml-dsa-65"; default_purpose = PURPOSE_PQ_SIG; break; case 7: default_name = "slh_dsa_128s"; default_path = "m/44'/102004'/0'/0'/0'"; default_curve_str = "slh-dsa-128s"; default_purpose = PURPOSE_PQ_SIG; break; case 8: default_name = "ml_kem_768"; default_path = "m/44'/102005'/0'/0'/0'"; default_curve_str = "ml-kem-768"; default_purpose = PURPOSE_PQ_KEM; break; case 9: /* OTP role — one-time pad encryption, no derivation path */ default_name = "otp"; default_path = ""; default_curve_str = "otp"; default_purpose = PURPOSE_NOSTR; /* not used for OTP */ break; case 10: /* Custom — will prompt for all fields */ default_name = "custom"; default_path = "m/44'/1237'/0'/0/0"; default_curve_str = "secp256k1"; default_purpose = PURPOSE_NOSTR; break; default: break; } /* Role name */ char role_name[ROLE_NAME_MAX]; { char prompt_buf[128]; snprintf(prompt_buf, sizeof(prompt_buf), " Role name [%s]: ", default_name); if (read_line_editable(role_name, sizeof(role_name), default_name, prompt_buf) != 0) { return (roles_created > 0) ? 0 : -1; } } { size_t len = strlen(role_name); while (len > 0 && (role_name[len-1] == '\n' || role_name[len-1] == '\r' || role_name[len-1] == ' ' || role_name[len-1] == '\t')) { role_name[--len] = '\0'; } } if (role_name[0] == '\0') { strncpy(role_name, default_name, sizeof(role_name) - 1); role_name[sizeof(role_name) - 1] = '\0'; } if (role_table_find_by_name(role_table, role_name) != NULL) { printf(" Role '%s' already exists, skipping.\n", role_name); continue; } /* OTP role (choice 9): one-time pad encryption. No curve, no * derivation path — instead we prompt for and bind the OTP pad. */ if (choice == 9) { char pad_dir[256]; char pad_spec[256]; printf(" OTP pad directory (e.g. /media/usb0): "); fflush(stdout); if (read_line_stdin(pad_dir, sizeof(pad_dir)) != 0) { return (roles_created > 0) ? 0 : -1; } { size_t len = strlen(pad_dir); while (len > 0 && (pad_dir[len-1] == '\n' || pad_dir[len-1] == '\r' || pad_dir[len-1] == ' ' || pad_dir[len-1] == '\t')) { pad_dir[--len] = '\0'; } } if (pad_dir[0] == '\0') { printf(" No pad directory entered, skipping OTP role.\n"); continue; } printf(" OTP pad name (e.g. mypad): "); fflush(stdout); if (read_line_stdin(pad_spec, sizeof(pad_spec)) != 0) { return (roles_created > 0) ? 0 : -1; } { size_t len = strlen(pad_spec); while (len > 0 && (pad_spec[len-1] == '\n' || pad_spec[len-1] == '\r' || pad_spec[len-1] == ' ' || pad_spec[len-1] == '\t')) { pad_spec[--len] = '\0'; } } if (pad_spec[0] == '\0') { printf(" No pad name entered, skipping OTP role.\n"); continue; } /* Bind the OTP pad immediately. allow_blkback=0 (warn later if needed). */ if (otp_pad_bind(pad_dir, pad_spec, 0) != 0) { printf(" Failed to bind OTP pad (dir=%s, spec=%s). Skipping role.\n", pad_dir, pad_spec); continue; } /* Remember the bound pad so the later OTP pad prompt is skipped. */ strncpy(g_wizard_otp_dir, pad_dir, sizeof(g_wizard_otp_dir) - 1); g_wizard_otp_dir[sizeof(g_wizard_otp_dir) - 1] = '\0'; strncpy(g_wizard_otp_spec, pad_spec, sizeof(g_wizard_otp_spec) - 1); g_wizard_otp_spec[sizeof(g_wizard_otp_spec) - 1] = '\0'; /* Role-as-password by default: knowing the role name is sufficient * authorization. No interactive approval prompt. */ int requires_approval = 0; /* Register the OTP role. No curve/path derivation; the pad is the key. */ role_entry_t new_role; memset(&new_role, 0, sizeof(new_role)); strncpy(new_role.name, role_name, sizeof(new_role.name) - 1); new_role.name[sizeof(new_role.name) - 1] = '\0'; /* OTP is identified by the "otp" algorithm string. */ strncpy(new_role.purpose_str, "nostr", sizeof(new_role.purpose_str) - 1); new_role.purpose_str[sizeof(new_role.purpose_str) - 1] = '\0'; strncpy(new_role.curve_str, "otp", sizeof(new_role.curve_str) - 1); new_role.curve_str[sizeof(new_role.curve_str) - 1] = '\0'; new_role.purpose = PURPOSE_NOSTR; /* not used for OTP */ new_role.curve = CURVE_UNKNOWN; /* OTP has no curve */ new_role.selector_type = SELECTOR_ROLE_PATH; new_role.role_path[0] = '\0'; /* OTP doesn't use a path */ new_role.nostr_index = -1; new_role.path_range_lo = -1; new_role.path_range_hi = -1; new_role.path_default_index = -1; new_role.requires_approval = requires_approval; new_role.path_allowed_count = 0; new_role.derived = 0; if (role_table_add(role_table, &new_role) != 0) { printf(" Failed to register role '%s' (table full or duplicate).\n", role_name); } else { roles_created++; printf(" Role '%s' registered: OTP pad bound (pad=%s, dir=%s, requires_approval=%d).\n", role_name, pad_spec, pad_dir, requires_approval); } /* Ask if user wants to define another role */ printf("Define another role? [y/N]: "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { break; } if (tolower((unsigned char)input[0]) != 'y') { break; } continue; } /* For custom path (choice 10), let user pick curve first, then path */ role_curve_t curve; role_purpose_t purpose; if (choice == 10) { printf(" Curve:\n"); printf(" 1) secp256k1 (Nostr, Bitcoin)\n"); printf(" 2) ed25519 (SSH)\n"); printf(" 3) x25519 (key agreement, Age)\n"); printf(" 4) ml-dsa-65 (post-quantum signatures)\n"); printf(" 5) slh-dsa-128s (post-quantum signatures)\n"); printf(" 6) ml-kem-768 (post-quantum KEM)\n"); printf(" Select [1]: "); fflush(stdout); char curve_choice[16]; if (read_line_stdin(curve_choice, sizeof(curve_choice)) != 0) { return (roles_created > 0) ? 0 : -1; } { size_t len = strlen(curve_choice); while (len > 0 && (curve_choice[len-1] == '\n' || curve_choice[len-1] == '\r' || curve_choice[len-1] == ' ' || curve_choice[len-1] == '\t')) { curve_choice[--len] = '\0'; } } int cchoice = 1; if (curve_choice[0] != '\0') { cchoice = atoi(curve_choice); if (cchoice < 1 || cchoice > 6) cchoice = 1; } switch (cchoice) { case 2: curve = CURVE_ED25519; default_path = "m/44'/102001'/0'/0'/0'"; break; case 3: curve = CURVE_X25519; default_path = "m/44'/102002'/0'/0'/0'"; break; case 4: curve = CURVE_ML_DSA_65; default_path = "m/44'/102003'/0'/0'/0'"; break; case 5: curve = CURVE_SLH_DSA_128S; default_path = "m/44'/102004'/0'/0'/0'"; break; case 6: curve = CURVE_ML_KEM_768; default_path = "m/44'/102005'/0'/0'/0'"; break; default: curve = CURVE_SECP256K1; break; } } else { curve = role_curve_from_str(default_curve_str); purpose = default_purpose; } /* Path template — only prompt for custom path; presets use the default */ char path_token[ROLE_PATH_MAX]; if (choice == 10) { { char prompt_buf[256]; snprintf(prompt_buf, sizeof(prompt_buf), " Path template [%s]: ", default_path); if (read_line_editable(path_token, sizeof(path_token), default_path, prompt_buf) != 0) { return (roles_created > 0) ? 0 : -1; } } { size_t len = strlen(path_token); while (len > 0 && (path_token[len-1] == '\n' || path_token[len-1] == '\r' || path_token[len-1] == ' ' || path_token[len-1] == '\t')) { path_token[--len] = '\0'; } } if (path_token[0] == '\0') { strncpy(path_token, default_path, sizeof(path_token) - 1); path_token[sizeof(path_token) - 1] = '\0'; } purpose = purpose_from_path(path_token); } else { strncpy(path_token, default_path, sizeof(path_token) - 1); path_token[sizeof(path_token) - 1] = '\0'; } /* Parse the path template */ char template[ROLE_PATH_MAX]; int range_lo, range_hi; int allowed_indices[64]; int allowed_count = 0; if (parse_path_template_for_role(path_token, template, sizeof(template), &range_lo, &range_hi, allowed_indices, 64, &allowed_count) != 0) { printf(" Invalid path template: '%s'.\n", path_token); continue; } /* Validate purpose+curve combination (skip for custom paths — the user * can combine any curve with any path) */ if (choice != 10 && choice != 9 && crypto_alg_from_role(curve, purpose) == CRYPTO_ALG_UNKNOWN) { printf(" Curve %s is not valid for this path prefix. Try a different curve.\n", role_curve_to_str(curve)); continue; } /* Role-as-password by default: knowing the role name is sufficient * authorization. No interactive approval prompt. */ int requires_approval = 0; /* No default index — the client always sends the full path, so keys * are derived on-demand when a request comes in. */ int default_index = -1; int is_fixed = (range_lo < 0); /* Register the role */ role_entry_t new_role; memset(&new_role, 0, sizeof(new_role)); strncpy(new_role.name, role_name, sizeof(new_role.name) - 1); new_role.name[sizeof(new_role.name) - 1] = '\0'; strncpy(new_role.purpose_str, role_purpose_to_str(purpose), sizeof(new_role.purpose_str) - 1); new_role.purpose_str[sizeof(new_role.purpose_str) - 1] = '\0'; strncpy(new_role.curve_str, role_curve_to_str(curve), sizeof(new_role.curve_str) - 1); new_role.curve_str[sizeof(new_role.curve_str) - 1] = '\0'; new_role.purpose = purpose; new_role.curve = curve; new_role.selector_type = SELECTOR_ROLE_PATH; strncpy(new_role.role_path, template, sizeof(new_role.role_path) - 1); new_role.role_path[sizeof(new_role.role_path) - 1] = '\0'; new_role.nostr_index = -1; new_role.path_range_lo = range_lo; new_role.path_range_hi = range_hi; new_role.path_default_index = default_index; new_role.requires_approval = requires_approval; if (allowed_count > 0) { int copy_n = allowed_count; if (copy_n > (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0]))) { copy_n = (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0])); } memcpy(new_role.path_allowed_indices, allowed_indices, (size_t)copy_n * sizeof(int)); new_role.path_allowed_count = copy_n; } else { new_role.path_allowed_count = 0; } new_role.derived = 0; if (role_table_add(role_table, &new_role) != 0) { printf(" Failed to register role '%s' (table full or duplicate).\n", role_name); } else { roles_created++; if (is_fixed) { printf(" Role '%s' registered: curve=%s path=%s (fixed, requires_approval=%d).\n", role_name, role_curve_to_str(curve), template, requires_approval); } else { printf(" Role '%s' registered: curve=%s path=%s (range %d-%d, requires_approval=%d).\n", role_name, role_curve_to_str(curve), template, range_lo, range_hi, requires_approval); } } /* Ask if user wants to define another role */ printf("Define another role? [y/N]: "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { break; } if (tolower((unsigned char)input[0]) != 'y') { break; } } /* Mandatory: at least one role must be defined */ if (roles_created == 0) { printf("At least one role must be defined.\n"); return -1; } return 0; } /* * Parse a path template token (e.g. "m/44'/1237'/0-3/1/0" or * "m/44'/1237'/1+34+54/1/0") into a template with %d placeholder and * allowed indices. Returns 0 on success, -1 on parse error. * * On success: * template_out — the path with %d replacing the numeric/range/set segment * range_lo/range_hi — set to the min/max of the allowed indices * allowed_indices_out / allowed_count_out — the explicit set (if set form * was used); allowed_count_out is 0 for pure range/single form */ static int parse_path_template_for_role(const char *token, char *template_out, size_t template_sz, int *range_lo, int *range_hi, int *allowed_indices_out, int max_allowed, int *allowed_count_out) { char buf[ROLE_PATH_MAX]; char *p; int found_range = 0; if (token == NULL || template_out == NULL || range_lo == NULL || range_hi == NULL || allowed_indices_out == NULL || allowed_count_out == NULL) { return -1; } strncpy(buf, token, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; *range_lo = 0; *range_hi = 0; *allowed_count_out = 0; template_out[0] = '\0'; p = buf; /* Copy up to and including the first '/' */ { char *first_slash = strchr(p, '/'); if (first_slash == NULL) { return -1; } size_t prefix_len = (size_t)(first_slash - p) + 1; if (prefix_len >= template_sz) { return -1; } memcpy(template_out, p, prefix_len); template_out[prefix_len] = '\0'; p = first_slash + 1; } while (p != NULL && *p != '\0') { char *next_slash = strchr(p, '/'); char seg[64]; size_t seg_len; if (next_slash != NULL) { seg_len = (size_t)(next_slash - p); } else { seg_len = strlen(p); } if (seg_len == 0 || seg_len >= sizeof(seg)) { return -1; } memcpy(seg, p, seg_len); seg[seg_len] = '\0'; if (!found_range) { /* Check for wildcard * (any non-negative integer) */ if (strcmp(seg, "*") == 0 || strcmp(seg, "*'") == 0 || strcmp(seg, "*h") == 0 || strcmp(seg, "*H") == 0) { int seg_hardened = (seg[1] == '\'' || seg[1] == 'h' || seg[1] == 'H'); found_range = 1; *range_lo = 0; *range_hi = INT_MAX; if (strlen(template_out) + 5 >= template_sz) return -1; strcat(template_out, "%d"); if (seg_hardened) strcat(template_out, "'"); strcat(template_out, "/"); p = (next_slash != NULL) ? next_slash + 1 : NULL; continue; } /* Check for range/set markers first. Only strip hardened marker * (' or h) from segments that contain - or + (range/set forms). * A plain number with ' (like 44') is a literal hardened constant, * NOT a variable. */ char *plus = strchr(seg, '+'); char *dash = strchr(seg, '-'); int is_range_or_set = (plus != NULL || dash != NULL); int seg_hardened = 0; if (is_range_or_set && seg_len > 0 && (seg[seg_len - 1] == '\'' || seg[seg_len - 1] == 'h' || seg[seg_len - 1] == 'H')) { seg_hardened = 1; seg[seg_len - 1] = '\0'; seg_len--; } if (plus != NULL) { /* Set form: "1+34+54" or "1+3-5+10" */ int set_count = 0; char *tok = seg; int set_ok = 1; while (tok != NULL && *tok != '\0') { char *next_plus = strchr(tok, '+'); if (next_plus != NULL) *next_plus = '\0'; char *sub_dash = strchr(tok, '-'); if (sub_dash != NULL) { *sub_dash = '\0'; char *e1 = NULL, *e2 = NULL; long lo = strtol(tok, &e1, 10); long hi = strtol(sub_dash + 1, &e2, 10); if (*e1 != '\0' || *e2 != '\0' || lo < 0 || hi < 0 || lo > hi) { set_ok = 0; break; } for (long vi = lo; vi <= hi && set_count < max_allowed; vi++) { allowed_indices_out[set_count++] = (int)vi; } } else { char *e = NULL; long val = strtol(tok, &e, 10); if (*e != '\0' || val < 0) { set_ok = 0; break; } if (set_count < max_allowed) { allowed_indices_out[set_count++] = (int)val; } } tok = (next_plus != NULL) ? next_plus + 1 : NULL; } if (set_ok && set_count > 0) { found_range = 1; *allowed_count_out = set_count; *range_lo = allowed_indices_out[0]; *range_hi = allowed_indices_out[set_count - 1]; if (strlen(template_out) + 5 >= template_sz) return -1; strcat(template_out, "%d"); if (seg_hardened) strcat(template_out, "'"); strcat(template_out, "/"); } else { if (strlen(template_out) + seg_len + 3 >= template_sz) return -1; strcat(template_out, seg); strcat(template_out, "/"); } } else if (dash != NULL) { /* Range form: "N-M" */ *dash = '\0'; char *e1 = NULL, *e2 = NULL; long lo = strtol(seg, &e1, 10); long hi = strtol(dash + 1, &e2, 10); if (*e1 != '\0' || *e2 != '\0' || lo < 0 || hi < 0 || lo > hi) { *dash = '-'; /* restore dash */ if (strlen(template_out) + seg_len + 3 >= template_sz) return -1; strcat(template_out, seg); strcat(template_out, "/"); } else { found_range = 1; *range_lo = (int)lo; *range_hi = (int)hi; if (strlen(template_out) + 5 >= template_sz) return -1; strcat(template_out, "%d"); if (seg_hardened) strcat(template_out, "'"); strcat(template_out, "/"); } } else { /* Single number — only treat as variable if NO hardened marker. * A segment like "44'" is a literal hardened constant. */ char *e = NULL; long v = strtol(seg, &e, 10); if (*e != '\0' || v < 0) { /* Not a plain number (has ' or other chars) — literal */ if (strlen(template_out) + seg_len + 3 >= template_sz) return -1; strcat(template_out, seg); strcat(template_out, "/"); } else { /* Plain number without ' — this is the variable */ found_range = 1; *range_lo = (int)v; *range_hi = (int)v; if (strlen(template_out) + 5 >= template_sz) return -1; strcat(template_out, "%d"); strcat(template_out, "/"); } } } else { if (strlen(template_out) + seg_len + 2 >= template_sz) return -1; strcat(template_out, seg); strcat(template_out, "/"); } p = (next_slash != NULL) ? next_slash + 1 : NULL; } /* Remove trailing '/' */ { size_t tlen = strlen(template_out); if (tlen > 0 && template_out[tlen - 1] == '/') { template_out[tlen - 1] = '\0'; } } if (!found_range) { /* Fixed path — no variable segment. Treat as a single fixed key. */ *range_lo = -1; *range_hi = -1; *allowed_count_out = 0; } return 0; } /* * Auto-detect purpose from a derivation path prefix. * m/44'/1237' → nostr, m/44'/102001' → ssh, etc. * Falls back to PURPOSE_NOSTR for unrecognized prefixes. */ static role_purpose_t purpose_from_path(const char *path) { if (path == NULL) return PURPOSE_NOSTR; if (strncmp(path, "m/44'/1237'", 11) == 0) return PURPOSE_NOSTR; if (strncmp(path, "m/44'/102001'", 13) == 0) return PURPOSE_SSH; if (strncmp(path, "m/44'/102002'", 13) == 0) return PURPOSE_AGE; if (strncmp(path, "m/44'/102003'", 13) == 0) return PURPOSE_PQ_SIG; if (strncmp(path, "m/44'/102004'", 13) == 0) return PURPOSE_PQ_SIG; if (strncmp(path, "m/44'/102005'", 13) == 0) return PURPOSE_PQ_KEM; if (strncmp(path, "m/84'", 4) == 0) return PURPOSE_BITCOIN; if (strncmp(path, "m/86'", 4) == 0) return PURPOSE_BITCOIN; return PURPOSE_NOSTR; /* default */ } /* * Parse a --register-role spec of the form: * :: * and register it into the role table. The curve field may be empty * (e.g. "name::path") in which case the curve is auto-detected from the * path prefix via purpose_from_path + the default curve for that purpose. * * The path-template uses the same syntax as the TUI wizard: * - Wildcard: * or *' (any non-negative integer) * - Range: 0-99 or 0-99' (inclusive range) * - Set: 1+3+5 or 1+3-5+10 * - Fixed: a literal path with no variable segment * * Returns 0 on success, -1 on parse error. */ static int register_role_from_spec(role_table_t *role_table, const char *spec) { char buf[512]; char *name, *curve_str, *path_token; role_curve_t curve; role_purpose_t purpose; char template[ROLE_PATH_MAX]; int range_lo, range_hi; int allowed_indices[64]; int allowed_count = 0; role_entry_t new_role; if (role_table == NULL || spec == NULL) { return -1; } strncpy(buf, spec, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; /* Split on ':' — name:curve:path-template */ name = buf; curve_str = strchr(buf, ':'); if (curve_str == NULL) { fprintf(stderr, "--register-role: missing ':' separator in '%s'\n", spec); return -1; } *curve_str = '\0'; curve_str++; path_token = strchr(curve_str, ':'); if (path_token == NULL) { fprintf(stderr, "--register-role: missing path template in '%s' (expected name:curve:path)\n", spec); return -1; } *path_token = '\0'; path_token++; if (name[0] == '\0') { fprintf(stderr, "--register-role: empty role name in '%s'\n", spec); return -1; } if (path_token[0] == '\0') { fprintf(stderr, "--register-role: empty path template in '%s'\n", spec); return -1; } /* Resolve curve: empty string → auto-detect from path */ if (curve_str[0] == '\0') { purpose = purpose_from_path(path_token); /* Default curve per purpose */ switch (purpose) { case PURPOSE_SSH: curve = CURVE_ED25519; break; case PURPOSE_AGE: curve = CURVE_X25519; break; case PURPOSE_PQ_SIG: /* 102003 → ml-dsa-65, 102004 → slh-dsa-128s */ if (strncmp(path_token, "m/44'/102004'", 13) == 0) { curve = CURVE_SLH_DSA_128S; } else { curve = CURVE_ML_DSA_65; } break; case PURPOSE_PQ_KEM: curve = CURVE_ML_KEM_768; break; default: curve = CURVE_SECP256K1; break; } } else { curve = role_curve_from_str(curve_str); if (curve == CURVE_UNKNOWN) { fprintf(stderr, "--register-role: unknown curve '%s' in '%s'\n", curve_str, spec); return -1; } purpose = purpose_from_path(path_token); } /* Parse the path template (wildcard/range/set → %d) */ if (parse_path_template_for_role(path_token, template, sizeof(template), &range_lo, &range_hi, allowed_indices, 64, &allowed_count) != 0) { fprintf(stderr, "--register-role: invalid path template '%s'\n", path_token); return -1; } /* Validate purpose+curve combination */ if (crypto_alg_from_role(curve, purpose) == CRYPTO_ALG_UNKNOWN) { fprintf(stderr, "--register-role: curve '%s' is not valid for path '%s'\n", role_curve_to_str(curve), path_token); return -1; } /* Register the role (no approval required in non-interactive mode) */ memset(&new_role, 0, sizeof(new_role)); strncpy(new_role.name, name, sizeof(new_role.name) - 1); new_role.name[sizeof(new_role.name) - 1] = '\0'; strncpy(new_role.purpose_str, role_purpose_to_str(purpose), sizeof(new_role.purpose_str) - 1); new_role.purpose_str[sizeof(new_role.purpose_str) - 1] = '\0'; strncpy(new_role.curve_str, role_curve_to_str(curve), sizeof(new_role.curve_str) - 1); new_role.curve_str[sizeof(new_role.curve_str) - 1] = '\0'; new_role.purpose = purpose; new_role.curve = curve; new_role.selector_type = SELECTOR_ROLE_PATH; strncpy(new_role.role_path, template, sizeof(new_role.role_path) - 1); new_role.role_path[sizeof(new_role.role_path) - 1] = '\0'; new_role.nostr_index = -1; new_role.path_range_lo = range_lo; new_role.path_range_hi = range_hi; new_role.path_default_index = -1; new_role.requires_approval = 0; /* non-interactive: no TUI approval */ if (allowed_count > 0) { int copy_n = allowed_count; if (copy_n > (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0]))) { copy_n = (int)(sizeof(new_role.path_allowed_indices) / sizeof(new_role.path_allowed_indices[0])); } memcpy(new_role.path_allowed_indices, allowed_indices, (size_t)copy_n * sizeof(int)); new_role.path_allowed_count = copy_n; } else { new_role.path_allowed_count = 0; } new_role.derived = 0; if (role_table_add(role_table, &new_role) != 0) { fprintf(stderr, "--register-role: failed to register role '%s' (table full or duplicate name)\n", name); return -1; } return 0; } 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); } } /* NSIGNER_TEST_HOTKEYS previously toggled g_auto_approve via the 'a' key. * The auto-approve TUI toggle has been removed (authorization is now * per-role via requires_approval). The env var is retained as a no-op * so existing test harnesses that set it do not break. */ (void)getenv("NSIGNER_TEST_HOTKEYS"); } /* Transport selection bitmask for interactive menu */ #define TRANSPORT_UNIX 0x01 #define TRANSPORT_QREXEC_BRIDGE 0x02 #define TRANSPORT_TCP 0x04 #define TRANSPORT_HTTP 0x08 #define TRANSPORT_QREXEC_ONESHOT 0x10 /* * 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. FIPS/TCP listener (framed JSON, FIPS mesh or local network)\n", (selected & TRANSPORT_TCP) ? "x" : " "); printf(" [%s] 4. HTTP listener (curl-friendly, localhost by default)\n", (selected & TRANSPORT_HTTP) ? "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 | TRANSPORT_HTTP; 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 if (input[0] == '4') { selected ^= TRANSPORT_HTTP; } else { printf("Invalid input. Type 1-4, '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, "Whitelist — restrict which nostr_index / role_path values this session can access"); printf("Enter allowed indices/paths, or press Enter for 'all' (no restriction):\n\n"); printf(" Examples:\n"); printf(" all (default — allow all)\n"); printf(" 0 (only nostr_index 0)\n"); printf(" 0-3 (nostr_index 0..3)\n"); printf(" m/44'/1237'/0-3/0/0 (NIP-06 paths X=0..3)\n"); printf(" m/44'/1237'/0-3/1/0 (custom paths X=0..3, change=1)\n"); printf(" m/44'/1237'/0-3/0/0,m/44'/1237'/0-3/1/0 (both)\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_path_whitelist(&tmp, input) != 0) { printf("Invalid spec: '%s'. Try again or press Enter for 'all'.\n", input); continue; } } return strdup(input); } } /* * Scan for OTP pads on mounted USB drives. * * Looks at /proc/mounts for mounts whose source device is /dev/sd* (USB mass * storage), then checks each mount point for a "pads/" subdirectory containing * .pad files. Also checks the mount point root itself for .pad files. * * Fills `out_dirs` (array of malloc'd pad-directory paths) and `out_pads` * (array of malloc'd pad chksum strings) up to max_entries. Returns the count * found, or 0 if none. */ #define OTP_SCAN_MAX 16 static int scan_usb_pads(char *out_dirs[][OTP_SCAN_MAX], char *out_pads[][OTP_SCAN_MAX]) { FILE *mtab = fopen("/proc/mounts", "r"); if (!mtab) return 0; char line[1024]; int count = 0; while (fgets(line, sizeof(line), mtab) && count < OTP_SCAN_MAX) { char source[512], mount[512], fstype[64]; if (sscanf(line, "%511s %511s %63s", source, mount, fstype) < 3) continue; /* Only look at /dev/sd* devices (USB mass storage). */ if (strncmp(source, "/dev/sd", 7) != 0) continue; /* Check /pads/ for .pad files, and also the mount root. */ const char *subdirs[] = {"pads", "" /* root */, NULL}; for (int s = 0; subdirs[s] != NULL && count < OTP_SCAN_MAX; s++) { char dir[600]; if (subdirs[s][0] == '\0') { snprintf(dir, sizeof(dir), "%s", mount); } else { snprintf(dir, sizeof(dir), "%s/%s", mount, subdirs[s]); } DIR *d = opendir(dir); if (!d) continue; struct dirent *e; while ((e = readdir(d)) != NULL && count < OTP_SCAN_MAX) { size_t nlen = strlen(e->d_name); if (nlen < 5 || strcmp(e->d_name + nlen - 4, ".pad") != 0) continue; /* Found a .pad file. The chksum is the filename without .pad. */ char chksum[128]; size_t clen = nlen - 4; if (clen >= sizeof(chksum)) continue; memcpy(chksum, e->d_name, clen); chksum[clen] = '\0'; (*out_dirs)[count] = strdup(dir); (*out_pads)[count] = strdup(chksum); count++; } closedir(d); } } fclose(mtab); return count; } /* * Interactive OTP pad selection prompt. * Shown after index whitelist when no --otp-pad-dir flag was given and * stdin is a TTY. Automatically scans attached USB drives for pads and * offers them. Falls back to manual directory entry if no pads are found. * * On success with a pad selected, returns 0 and fills: * *out_pad_dir — malloc'd pad directory path (caller frees) * *out_pad_spec — malloc'd pad chksum/prefix (caller frees) * *out_allow_blkback — 1 if the user acknowledged blkback, 0 otherwise * Returns 1 if the user declined (no pad), -1 on read error. */ static int prompt_otp_pad(char **out_pad_dir, char **out_pad_spec, int *out_allow_blkback) { char input[512]; *out_pad_dir = NULL; *out_pad_spec = NULL; *out_allow_blkback = 0; /* Auto-scan USB drives for pads. */ char *found_dirs[OTP_SCAN_MAX]; char *found_pads[OTP_SCAN_MAX]; int found_count = scan_usb_pads(&found_dirs, &found_pads); if (found_count > 0) { /* Offer the discovered pads. */ for (;;) { tui_render_content_screen(NULL, "OTP pad — pads found on USB drives"); printf("Found %d pad(s) on attached USB drive(s):\n\n", found_count); for (int i = 0; i < found_count; i++) { printf(" %d. %s (in %s)\n", i + 1, found_pads[i], found_dirs[i]); } printf("\n Type a number to select, 'm' for manual entry, 'n' to skip\n"); printf(" Enter = 1 (first pad)\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { for (int i = 0; i < found_count; i++) { free(found_dirs[i]); free(found_pads[i]); } return -1; } { 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') { /* Default: first pad. */ *out_pad_dir = strdup(found_dirs[0]); *out_pad_spec = strdup(found_pads[0]); break; } if (input[0] == 'n' || input[0] == 'N') { for (int i = 0; i < found_count; i++) { free(found_dirs[i]); free(found_pads[i]); } return 1; /* declined */ } if (input[0] == 'm' || input[0] == 'M') { /* Fall through to manual entry. */ break; } /* Try to parse a number. */ int choice = atoi(input); if (choice >= 1 && choice <= found_count) { *out_pad_dir = strdup(found_dirs[choice - 1]); *out_pad_spec = strdup(found_pads[choice - 1]); break; } printf("Invalid selection. Type 1-%d, 'm', or 'n'.\n", found_count); } /* If the user selected a pad (not manual), we're done. */ if (*out_pad_dir != NULL) { for (int i = 0; i < found_count; i++) { free(found_dirs[i]); free(found_pads[i]); } /* USB drives (/dev/sd*) are not blkback, so no warning needed. */ return 0; } /* Manual entry requested — free the scan results and fall through. */ for (int i = 0; i < found_count; i++) { free(found_dirs[i]); free(found_pads[i]); } } /* No pads found on USB, or user chose manual entry. */ for (;;) { tui_render_content_screen(NULL, "OTP pad — bind a one-time pad?"); if (found_count > 0) { printf("Manual pad directory entry:\n\n"); } else { printf("No pads found on attached USB drives.\n"); printf("Do you want to manually specify a pad directory? (y/n)\n\n"); printf(" Enter = no\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { return -1; } { 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' || input[0] == 'n' || input[0] == 'N') { return 1; /* declined */ } if (input[0] != 'y' && input[0] != 'Y') { printf("Please type 'y' or 'n'.\n"); continue; } } /* Prompt for pad directory. */ tui_render_content_screen(NULL, "OTP pad — pad directory"); printf("Enter the directory containing the .pad and .state files:\n\n"); printf(" Examples:\n"); printf(" /media/user/Music/pads\n"); printf(" /mnt/usb/pads\n"); printf("\n (This is typically a directory on a USB drive.)\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { return -1; } { 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') { printf("No directory entered. Skipping OTP pad.\n"); return 1; } struct stat st; if (stat(input, &st) != 0 || !S_ISDIR(st.st_mode)) { printf("Directory '%s' not found or not a directory. Try again.\n", input); continue; } *out_pad_dir = strdup(input); break; } /* Prompt for pad checksum or prefix. */ for (;;) { tui_render_content_screen(NULL, "OTP pad — pad checksum or prefix"); printf("Enter the pad checksum (64 hex chars) or a unique prefix:\n\n"); printf(" The pad file is named .pad in the directory.\n"); printf(" You can enter the first few characters if unique.\n"); printf(" (Press Enter to list available pads in the directory.)\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { free(*out_pad_dir); *out_pad_dir = NULL; return -1; } { 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') { /* List available pads. */ DIR *d = opendir(*out_pad_dir); if (!d) { printf("Cannot open directory '%s'. Try again.\n", *out_pad_dir); continue; } printf("\nAvailable pads in %s:\n", *out_pad_dir); struct dirent *e; int count = 0; while ((e = readdir(d)) != NULL) { size_t nlen = strlen(e->d_name); if (nlen > 4 && strcmp(e->d_name + nlen - 4, ".pad") == 0) { printf(" %.*s\n", (int)(nlen - 4), e->d_name); count++; } } closedir(d); if (count == 0) { printf(" (no .pad files found)\n"); } printf("\n"); continue; } *out_pad_spec = strdup(input); break; } /* Check if the pad directory is on a blkback device and warn. */ { const char *dir = *out_pad_dir; FILE *mtab = fopen("/proc/mounts", "r"); int is_blkback = 0; if (mtab) { char line[1024]; size_t best_len = 0; while (fgets(line, sizeof(line), mtab)) { char source[512], mount[512], fstype[64]; if (sscanf(line, "%511s %511s %63s", source, mount, fstype) < 3) continue; size_t mlen = strlen(mount); if (strncmp(dir, mount, mlen) == 0 && (dir[mlen] == '/' || dir[mlen] == '\0') && mlen > best_len) { best_len = mlen; is_blkback = (strncmp(source, "/dev/xvd", 8) == 0); } } fclose(mtab); } if (is_blkback) { tui_render_content_screen(NULL, "OTP pad — blkback warning"); printf("WARNING: %s appears to be on a blkback device (qvm-block).\n", dir); printf("For pad secrecy, PCI USB controller passthrough is recommended.\n"); printf("Continue anyway? (y/n)\n"); printf("> "); fflush(stdout); if (read_line_stdin(input, sizeof(input)) != 0) { free(*out_pad_dir); free(*out_pad_spec); *out_pad_dir = NULL; *out_pad_spec = NULL; return -1; } if (input[0] == 'y' || input[0] == 'Y') { *out_allow_blkback = 1; } else { printf("OTP pad binding cancelled.\n"); free(*out_pad_dir); free(*out_pad_spec); *out_pad_dir = NULL; *out_pad_spec = NULL; return 1; } } } return 0; } 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[4]; /* max: 3 listeners (unix+tcp+http) + 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; const char *register_role_specs[ROLE_TABLE_MAX_ENTRIES]; int register_role_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; mnemonic_source_t mnemonic_source = { MNEMONIC_SOURCE_TUI, -1 }; const char *otp_pad_dir = NULL; const char *otp_pad_spec = NULL; int otp_allow_blkback = 0; 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 if (strncmp(argv[argi + 1], "http:", 5) == 0) { listen_mode = NSIGNER_LISTEN_HTTP; listen_target = argv[argi + 1]; } else { fprintf(stderr, "Invalid --listen mode: %s (expected unix|stdio|qrexec|tcp:HOST:PORT|http: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], "--register-role") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } if (register_role_count >= ROLE_TABLE_MAX_ENTRIES) { fprintf(stderr, "Too many --register-role entries (max %d)\n", ROLE_TABLE_MAX_ENTRIES); return 1; } register_role_specs[register_role_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], "--allow-unlocked-memory") == 0) { secure_buf_allow_unlocked(); argi += 1; continue; } if (strcmp(argv[argi], "--bridge-source-trusted") == 0) { bridge_source_trusted = 1; argi += 1; continue; } if (strcmp(argv[argi], "--otp-pad-dir") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } otp_pad_dir = argv[argi + 1]; argi += 2; continue; } if (strcmp(argv[argi], "--otp-pad") == 0) { if (argi + 1 >= argc) { fprintf(stderr, "Missing value for %s\n", argv[argi]); return 1; } otp_pad_spec = argv[argi + 1]; argi += 2; continue; } if (strcmp(argv[argi], "--otp-allow-blkback") == 0) { otp_allow_blkback = 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], "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); /* Interactive role definition wizard (only in TUI mode) */ if (mnemonic_source.kind == MNEMONIC_SOURCE_TUI && isatty(STDIN_FILENO)) { if (prompt_named_path_roles(&role_table) != 0) { fprintf(stderr, "At least one role must be defined.\n"); mnemonic_unload(&mnemonic); return 1; } } else { /* Non-interactive mode. If --register-role specs were provided, * register those roles (suppressing the default "main" role). * Otherwise, create a default "main" role with the standard * NIP-06 path. */ if (register_role_count > 0) { for (int i = 0; i < register_role_count; ++i) { if (register_role_from_spec(&role_table, register_role_specs[i]) != 0) { fprintf(stderr, "Failed to register role from --register-role spec\n"); mnemonic_unload(&mnemonic); return 1; } } } else { role_entry_t role; 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_ROLE_PATH; strncpy(role.role_path, "m/44'/1237'/0'/0/0", sizeof(role.role_path) - 1); role.role_path[sizeof(role.role_path) - 1] = '\0'; role.nostr_index = -1; role.path_range_lo = -1; role.path_range_hi = -1; role.path_default_index = -1; role.requires_approval = 0; /* role-as-password: knowing the role name is sufficient */ role.derived = 0; if (role_table_add(&role_table, &role) != 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); /* Bind OTP pad if requested. One pad per session. */ if (otp_pad_dir != NULL) { if (otp_pad_spec == NULL) { fprintf(stderr, "nsigner: --otp-pad-dir requires --otp-pad \n"); crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } if (otp_pad_bind(otp_pad_dir, otp_pad_spec, otp_allow_blkback) != 0) { fprintf(stderr, "nsigner: failed to bind OTP pad\n"); crypto_wipe(&key_store); alg_key_cache_wipe(&alg_key_cache); nostr_cleanup(); mnemonic_unload(&mnemonic); return 1; } } /* * 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[3]; /* max: unix + tcp + http */ 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; } /* OTP pad binding is now handled in the role wizard (option 9). * The standalone OTP pad prompt has been removed. If a pad was bound * via the wizard, use it; otherwise --otp-pad-dir from CLI still works. */ if (otp_pad_dir == NULL && g_wizard_otp_dir[0] != '\0') { otp_pad_dir = g_wizard_otp_dir; otp_pad_spec = g_wizard_otp_spec; } /* 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 || listen_mode == NSIGNER_LISTEN_HTTP) && 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; } /* HTTP mode: no auth envelopes by default (rely on localhost + policy). */ 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 (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 || listen_mode == NSIGNER_LISTEN_HTTP) { 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:[::]:11111"; 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 (server_start(&servers[tcp_server_idx]) != 0) { fprintf(stderr, "Failed to start FIPS/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++; } /* HTTP listener (multi-listener mode) */ int http_server_idx = -1; if (multi_listen && (transport_mask & TRANSPORT_HTTP)) { const char *http_target = "http:127.0.0.1:11111"; http_server_idx = server_count; server_init(&servers[http_server_idx], http_target, 0, NSIGNER_LISTEN_HTTP, NSIGNER_AUTH_OFF, auth_skew_seconds, &dispatcher, &policy); if (server_start(&servers[http_server_idx]) != 0) { fprintf(stderr, "Failed to start HTTP server on %s: %s\n", http_target, server_last_error(&servers[http_server_idx])); /* Stop already-started servers */ for (int si = 0; si < server_count; si++) server_stop(&servers[si]); 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)) { char unix_conn[128]; char unix_example[256]; char unix_extra[256] = ""; snprintf(unix_conn, sizeof(unix_conn), "%s", socket_name); snprintf(unix_example, sizeof(unix_example), "nsigner --socket-name %s client ''", socket_name); if (transport_mask & TRANSPORT_QREXEC_BRIDGE) { snprintf(unix_extra, sizeof(unix_extra), "Qrexec (bridge-source-trusted):\n" " qrexec-client-vm %s", NSIGNER_QREXEC_SERVICE_NAME); printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME); } connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra); printf("System is ready and waiting for connections on %s.\n", socket_name); } if (transport_mask & TRANSPORT_TCP) { const char *tcp_addr = servers[tcp_server_idx].socket_name; char fips_conn[256] = ""; char fips_example[256] = ""; /* If we have a FIPS identity, show the npub-based URL */ if (have_fips_identity && extract_listen_port(tcp_addr, listen_port, sizeof(listen_port)) == 0) { snprintf(fips_conn, sizeof(fips_conn), "http://%s.fips:%s", fips_npub, listen_port); snprintf(fips_example, sizeof(fips_example), "curl -X POST http://%s.fips:%s/ \\\n" " -H 'Content-Type: application/json' \\\n" " -d ''", fips_npub, listen_port); printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port); } else { snprintf(fips_conn, sizeof(fips_conn), "%s", tcp_addr); snprintf(fips_example, sizeof(fips_example), "nsigner --listen %s client ''", tcp_addr); } connection_info_add_transport("FIPS", fips_conn, fips_example, ""); printf("System is ready and waiting for connections on %s.\n", tcp_addr); } if (transport_mask & TRANSPORT_HTTP) { const char *http_addr = servers[http_server_idx].socket_name; char http_conn[256]; char http_example[256]; snprintf(http_conn, sizeof(http_conn), "http://%s", http_addr + 5); snprintf(http_example, sizeof(http_example), "curl -X POST http://%s/ \\\n" " -H 'Content-Type: application/json' \\\n" " -d ''", http_addr + 5); connection_info_add_transport("HTTP", http_conn, http_example, ""); printf("System is ready and waiting for connections on http://%s.\n", http_addr + 5); } } else if (listen_mode == NSIGNER_LISTEN_UNIX) { char unix_conn[128]; char unix_example[256]; char unix_extra[256] = ""; snprintf(unix_conn, sizeof(unix_conn), "%s", socket_name); snprintf(unix_example, sizeof(unix_example), "nsigner --socket-name %s client ''", socket_name); if (bridge_source_trusted) { snprintf(unix_extra, sizeof(unix_extra), "Qrexec (bridge-source-trusted):\n" " qrexec-client-vm %s", NSIGNER_QREXEC_SERVICE_NAME); printf("qrexec service: %s\n", NSIGNER_QREXEC_SERVICE_NAME); } connection_info_add_transport("Unix socket", unix_conn, unix_example, unix_extra); printf("System is ready and waiting for connections on %s.\n", socket_name); } else if (listen_mode == NSIGNER_LISTEN_TCP) { const char *actual_addr = server.socket_name; char fips_conn[256] = ""; char fips_example[256] = ""; if (have_fips_identity && extract_listen_port(actual_addr, listen_port, sizeof(listen_port)) == 0) { snprintf(fips_conn, sizeof(fips_conn), "http://%s.fips:%s", fips_npub, listen_port); snprintf(fips_example, sizeof(fips_example), "curl -X POST http://%s.fips:%s/ \\\n" " -H 'Content-Type: application/json' \\\n" " -d ''", fips_npub, listen_port); printf("fips address: http://%s.fips:%s\n", fips_npub, listen_port); } else { snprintf(fips_conn, sizeof(fips_conn), "%s", actual_addr); snprintf(fips_example, sizeof(fips_example), "nsigner --listen %s client ''", actual_addr); } connection_info_add_transport("FIPS", fips_conn, fips_example, ""); printf("System is ready and waiting for connections on %s.\n", actual_addr); } else if (listen_mode == NSIGNER_LISTEN_HTTP) { const char *actual_addr = server.socket_name; char http_conn[256]; char http_example[256]; snprintf(http_conn, sizeof(http_conn), "http://%s", actual_addr + 5); snprintf(http_example, sizeof(http_example), "curl -X POST http://%s/ \\\n" " -H 'Content-Type: application/json' \\\n" " -d ''", actual_addr + 5); connection_info_add_transport("HTTP", http_conn, http_example, ""); printf("System is ready and waiting for connections on %s.\n", actual_addr); } else if (listen_mode == NSIGNER_LISTEN_QREXEC) { connection_info_add_transport("Qrexec", "qrexec (one request per invocation)", "", ""); printf("System is ready and waiting for a qrexec request.\n"); } else { connection_info_add_transport("Stdio", "stdio (one request per invocation)", "", ""); printf("System is ready and waiting for a stdio request.\n"); } /* Show OTP pad status if bound. */ if (otp_pad_is_bound()) { connection_info_set_otp("OTP pad: %s (offset %llu / %llu bytes)", otp_pad_chksum(), (unsigned long long)otp_pad_current_offset(), (unsigned long long)otp_pad_size()); } if (have_fips_identity) { printf("fips ipv6: %s\n", fips_ipv6); printf("fips npub: %s\n", fips_npub); } } 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 || listen_mode == NSIGNER_LISTEN_HTTP) { 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(); otp_pad_unbind(); mnemonic_unload(&mnemonic); return 0; } memset(&g_activity_log, 0, sizeof(g_activity_log)); /* --allow-all maps to server_set_prompt_always_allow (needed by tests and * non-interactive modes). The TUI auto-approve toggle has been removed; * authorization is now per-role via requires_approval. */ server_set_prompt_always_allow(allow_all ? 1 : 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); /* 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 (lower == 'd') { /* Display connection instructions on demand. * Show the connections screen, wait for any key, then * return to the normal status display. * * Use tui_get_key() rather than a bare read() so the * wait survives EINTR (e.g. SIGWINCH); a bare read() * returns immediately on signal interruption, which * causes render_status() to run right away and scroll * the connections view off the screen before the user * has a chance to read it. */ render_connections(&role_table, &mnemonic, derived_count, socket_name); (void)tui_get_key(); 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(); otp_pad_unbind(); mnemonic_unload(&mnemonic); printf("Shutdown. All secrets wiped.\n"); return 0; }