1822 lines
60 KiB
C
1822 lines
60 KiB
C
#define _GNU_SOURCE
|
|
|
|
/* NSIGNER_HEADERLESS_DECLS_BEGIN */
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <sys/types.h>
|
|
#include <cJSON.h>
|
|
|
|
/* from secure_mem.h */
|
|
|
|
|
|
/*
|
|
* Secure memory buffer — mlock'd, zeroized on free.
|
|
* Used for mnemonic phrases, private keys, and any sensitive material.
|
|
*/
|
|
typedef struct {
|
|
void *data; /* pointer to locked allocation */
|
|
size_t size; /* usable size in bytes */
|
|
int locked; /* 1 if mlock succeeded */
|
|
} secure_buf_t;
|
|
|
|
/* Allocate a secure buffer of `size` bytes. Returns 0 on success, -1 on failure. */
|
|
int secure_buf_alloc(secure_buf_t *buf, size_t size);
|
|
|
|
/* Zeroize and free a secure buffer. Always succeeds (idempotent). */
|
|
void secure_buf_free(secure_buf_t *buf);
|
|
|
|
/* Zeroize `len` bytes at `ptr` in a way the compiler cannot optimize away. */
|
|
void secure_memzero(void *ptr, size_t len);
|
|
|
|
|
|
/* from mnemonic.h */
|
|
|
|
|
|
/* Maximum mnemonic length: 24 words * 10 chars avg + spaces + null = 256 is safe */
|
|
#define MNEMONIC_MAX_LEN 256
|
|
|
|
/*
|
|
* Mnemonic state — holds the loaded mnemonic in secure memory.
|
|
* Only one mnemonic is active at a time per process.
|
|
*/
|
|
typedef struct {
|
|
secure_buf_t buf; /* secure storage for the mnemonic string */
|
|
int loaded; /* 1 if a mnemonic is currently loaded */
|
|
int word_count; /* 12, 15, 18, 21, or 24 */
|
|
} mnemonic_state_t;
|
|
|
|
/* Initialize mnemonic state (must be called before use). */
|
|
void mnemonic_init(mnemonic_state_t *state);
|
|
|
|
/* Load a mnemonic string into secure memory. Validates word count (12/15/18/21/24).
|
|
* Returns 0 on success, -1 on invalid input, -2 on memory error. */
|
|
int mnemonic_load(mnemonic_state_t *state, const char *phrase);
|
|
|
|
/* Zeroize and unload the mnemonic. Idempotent. */
|
|
void mnemonic_unload(mnemonic_state_t *state);
|
|
|
|
/* Check if a mnemonic is currently loaded. */
|
|
int mnemonic_is_loaded(const mnemonic_state_t *state);
|
|
|
|
/* Get the mnemonic string (only valid while loaded). Returns NULL if not loaded. */
|
|
const char *mnemonic_get_phrase(const mnemonic_state_t *state);
|
|
|
|
/* Generate a new BIP-39 mnemonic phrase (12/15/18/21/24 words) into out.
|
|
* Returns 0 on success, -1 on invalid arguments or generation failure. */
|
|
int mnemonic_generate(int word_count, char *out, size_t out_len);
|
|
|
|
|
|
/* from role_table.h */
|
|
|
|
|
|
/* Maximum limits */
|
|
#define ROLE_NAME_MAX 64
|
|
#define ROLE_PATH_MAX 128
|
|
#define ROLE_PURPOSE_MAX 32
|
|
#define ROLE_CURVE_MAX 16
|
|
#define ROLE_PUBKEY_HEX_MAX 66 /* 64 hex chars + null + pad */
|
|
#define ROLE_TABLE_MAX_ENTRIES 256
|
|
|
|
/* Purpose enum for fast comparison (string form kept for config/display) */
|
|
typedef enum {
|
|
PURPOSE_NOSTR = 0,
|
|
PURPOSE_BITCOIN,
|
|
PURPOSE_SSH,
|
|
PURPOSE_AGE,
|
|
PURPOSE_FIPS,
|
|
PURPOSE_UNKNOWN
|
|
} role_purpose_t;
|
|
|
|
/* Curve enum */
|
|
typedef enum {
|
|
CURVE_SECP256K1 = 0,
|
|
CURVE_ED25519,
|
|
CURVE_X25519,
|
|
CURVE_UNKNOWN
|
|
} role_curve_t;
|
|
|
|
/* Selector type — how this role's key is addressed */
|
|
typedef enum {
|
|
SELECTOR_NOSTR_INDEX, /* uses nostr_index shorthand */
|
|
SELECTOR_ROLE_PATH /* uses explicit full path */
|
|
} role_selector_type_t;
|
|
|
|
/* A single role entry */
|
|
typedef struct {
|
|
char name[ROLE_NAME_MAX];
|
|
char purpose_str[ROLE_PURPOSE_MAX];
|
|
char curve_str[ROLE_CURVE_MAX];
|
|
role_purpose_t purpose;
|
|
role_curve_t curve;
|
|
role_selector_type_t selector_type;
|
|
int nostr_index; /* valid if selector_type == SELECTOR_NOSTR_INDEX */
|
|
char role_path[ROLE_PATH_MAX]; /* valid if selector_type == SELECTOR_ROLE_PATH */
|
|
char pubkey_hex[ROLE_PUBKEY_HEX_MAX]; /* filled after derivation, empty until then */
|
|
int derived; /* 1 if pubkey_hex has been populated */
|
|
} role_entry_t;
|
|
|
|
/* The role table */
|
|
typedef struct {
|
|
role_entry_t entries[ROLE_TABLE_MAX_ENTRIES];
|
|
int count;
|
|
} role_table_t;
|
|
|
|
/* Initialize an empty role table */
|
|
void role_table_init(role_table_t *table);
|
|
|
|
/* Add a role entry. Returns 0 on success, -1 if table full, -2 if name duplicate. */
|
|
int role_table_add(role_table_t *table, const role_entry_t *entry);
|
|
|
|
/* Find a role by name. Returns pointer to entry or NULL. */
|
|
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name);
|
|
|
|
/* Find a role by nostr_index. Returns pointer or NULL. */
|
|
role_entry_t *role_table_find_by_nostr_index(role_table_t *table, int index);
|
|
|
|
/* Find a role by role_path. Returns pointer or NULL. */
|
|
role_entry_t *role_table_find_by_path(role_table_t *table, const char *path);
|
|
|
|
/* Get the default role (named "main"). Returns pointer or NULL if no "main" role. */
|
|
role_entry_t *role_table_get_default(role_table_t *table);
|
|
|
|
/* Parse purpose string to enum */
|
|
role_purpose_t role_purpose_from_str(const char *s);
|
|
|
|
/* Parse curve string to enum */
|
|
role_curve_t role_curve_from_str(const char *s);
|
|
|
|
/* Purpose enum to string */
|
|
const char *role_purpose_to_str(role_purpose_t p);
|
|
|
|
/* Curve enum to string */
|
|
const char *role_curve_to_str(role_curve_t c);
|
|
|
|
/* Register a nostr-index role if missing. Returns 0 on success, -1 on error. */
|
|
int role_table_register_nostr_index(role_table_t *table, int nostr_index);
|
|
|
|
|
|
/* from selector.h */
|
|
|
|
|
|
/* Error codes for selector resolution */
|
|
#define SELECTOR_OK 0
|
|
#define SELECTOR_ERR_AMBIGUOUS -1 /* multiple selectors specified */
|
|
#define SELECTOR_ERR_NOT_FOUND -2 /* no matching role in table */
|
|
#define SELECTOR_ERR_NO_DEFAULT -3 /* no selector given and no "main" role exists */
|
|
|
|
/* Parsed selector from a request's options object */
|
|
typedef struct {
|
|
int has_role; /* 1 if "role" field was present */
|
|
char role_name[ROLE_NAME_MAX];
|
|
|
|
int has_nostr_index; /* 1 if "nostr_index" field was present */
|
|
int nostr_index;
|
|
|
|
int has_role_path; /* 1 if "role_path" field was present */
|
|
char role_path[ROLE_PATH_MAX];
|
|
} selector_request_t;
|
|
|
|
/* Initialize a selector request (all fields zeroed/unset) */
|
|
void selector_request_init(selector_request_t *req);
|
|
|
|
/*
|
|
* Resolve a selector request against the role table.
|
|
* On success (returns SELECTOR_OK), *out points to the matched role_entry_t.
|
|
* On failure, returns one of the SELECTOR_ERR_* codes and *out is NULL.
|
|
*/
|
|
int selector_resolve(const selector_request_t *req, role_table_t *table, role_entry_t **out);
|
|
|
|
/*
|
|
* Return a human-readable error string for a selector error code.
|
|
*/
|
|
const char *selector_strerror(int err);
|
|
|
|
|
|
/* from enforcement.h */
|
|
|
|
|
|
/* Error codes */
|
|
#define ENFORCE_OK 0
|
|
#define ENFORCE_ERR_PURPOSE -1 /* purpose mismatch */
|
|
#define ENFORCE_ERR_CURVE -2 /* curve mismatch */
|
|
#define ENFORCE_ERR_UNKNOWN_VERB -3 /* verb not recognized */
|
|
|
|
/* Known verbs */
|
|
#define VERB_SIGN_EVENT "sign_event"
|
|
#define VERB_GET_PUBLIC_KEY "get_public_key"
|
|
#define VERB_NIP44_ENCRYPT "nip44_encrypt"
|
|
#define VERB_NIP44_DECRYPT "nip44_decrypt"
|
|
#define VERB_NIP04_ENCRYPT "nip04_encrypt"
|
|
#define VERB_NIP04_DECRYPT "nip04_decrypt"
|
|
|
|
/*
|
|
* Check whether `verb` is allowed to execute against `role`.
|
|
* Returns ENFORCE_OK if allowed, or an ENFORCE_ERR_* code.
|
|
*
|
|
* The enforcement rules are:
|
|
* - All nostr verbs (sign_event, get_public_key, nip44_*, nip04_*) require:
|
|
* purpose == PURPOSE_NOSTR and curve == CURVE_SECP256K1
|
|
* - Unknown verbs return ENFORCE_ERR_UNKNOWN_VERB (fail-closed).
|
|
*/
|
|
int enforce_verb_role(const char *verb, const role_entry_t *role);
|
|
|
|
/*
|
|
* Return a human-readable error string for an enforcement error code.
|
|
*/
|
|
const char *enforce_strerror(int err);
|
|
|
|
|
|
/* from policy.h */
|
|
|
|
|
|
#define POLICY_MAX_ENTRIES 32
|
|
#define POLICY_MAX_VERBS 16
|
|
#define POLICY_MAX_ROLES 16
|
|
#define POLICY_MAX_PURPOSES 8
|
|
#define POLICY_VERB_MAX_LEN 32
|
|
#define POLICY_CALLER_MAX_LEN 160
|
|
|
|
/* Prompt behavior */
|
|
typedef enum {
|
|
PROMPT_NEVER = 0,
|
|
PROMPT_FIRST_PER_BOOT,
|
|
PROMPT_EVERY_REQUEST,
|
|
PROMPT_DENY
|
|
} prompt_mode_t;
|
|
|
|
typedef enum {
|
|
POLICY_SOURCE_DEFAULT = 0, /* the catch-all entry */
|
|
POLICY_SOURCE_PREAPPROVE, /* from --preapprove CLI flag */
|
|
POLICY_SOURCE_SESSION_GRANT /* from prompt [a] during session */
|
|
} policy_source_t;
|
|
|
|
/* A single policy entry */
|
|
typedef struct {
|
|
char caller[POLICY_CALLER_MAX_LEN]; /* e.g. "uid:1000" or "*" for any */
|
|
char verbs[POLICY_MAX_VERBS][POLICY_VERB_MAX_LEN];
|
|
int verb_count;
|
|
char roles[POLICY_MAX_ROLES][ROLE_NAME_MAX];
|
|
int role_count;
|
|
char purposes[POLICY_MAX_PURPOSES][ROLE_PURPOSE_MAX];
|
|
int purpose_count;
|
|
prompt_mode_t prompt;
|
|
policy_source_t source;
|
|
} policy_entry_t;
|
|
|
|
/* Policy table */
|
|
typedef struct {
|
|
policy_entry_t entries[POLICY_MAX_ENTRIES];
|
|
int count;
|
|
} policy_table_t;
|
|
|
|
/* Policy check result */
|
|
#define POLICY_ALLOW 0
|
|
#define POLICY_ALLOW_SESSION_VERB 3 /* allow + save session grant for this caller+role+verb */
|
|
#define POLICY_ALLOW_SESSION_ALL 4 /* allow + save session grant for this caller+role (all verbs) */
|
|
#define POLICY_DENY -1
|
|
#define POLICY_PROMPT -2 /* would need user confirmation */
|
|
#define POLICY_NO_MATCH -3 /* no policy entry matched (fail-closed = deny) */
|
|
|
|
/* Initialize policy table */
|
|
void policy_table_init(policy_table_t *table);
|
|
|
|
/* Initialize default policy: allow same-uid, deny others */
|
|
void policy_init_default(policy_table_t *table, uid_t owner_uid);
|
|
|
|
/* Add a policy entry. Returns 0 on success, -1 if full. */
|
|
int policy_table_add(policy_table_t *table, const policy_entry_t *entry);
|
|
|
|
/* Insert an entry before the final catch-all rule. */
|
|
int policy_table_insert_before_last(policy_table_t *table, const policy_entry_t *entry);
|
|
|
|
/*
|
|
* Check whether caller_id is allowed to invoke `verb` on `role_name` with given `purpose`.
|
|
* Returns POLICY_ALLOW, POLICY_DENY, POLICY_PROMPT, or POLICY_NO_MATCH.
|
|
*/
|
|
int policy_check(const policy_table_t *table, const char *caller_id,
|
|
const char *verb, const char *role_name, const char *purpose,
|
|
policy_source_t *out_source);
|
|
|
|
/* Parse prompt mode from string */
|
|
prompt_mode_t prompt_mode_from_str(const char *s);
|
|
|
|
/* Prompt mode to string */
|
|
const char *prompt_mode_to_str(prompt_mode_t m);
|
|
|
|
|
|
/* from crypto.h */
|
|
|
|
|
|
/* Per-role derived key material (stored in secure memory) */
|
|
typedef struct {
|
|
secure_buf_t private_key; /* 32 bytes, mlock'd */
|
|
unsigned char public_key[32];
|
|
char pubkey_hex[65]; /* 64 hex chars + null */
|
|
char npub[128]; /* bech32 npub */
|
|
int valid;
|
|
} derived_key_t;
|
|
|
|
/* Key store — holds derived keys for all roles */
|
|
typedef struct {
|
|
derived_key_t keys[ROLE_TABLE_MAX_ENTRIES];
|
|
int count;
|
|
} key_store_t;
|
|
|
|
/* Derive keys for all roles in the table using the loaded mnemonic.
|
|
* Populates key_store and sets role->pubkey_hex and role->derived for each role.
|
|
* Only derives for roles with purpose=nostr and curve=secp256k1 (for now).
|
|
* Returns number of keys derived, or -1 on error. */
|
|
int crypto_derive_all(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic);
|
|
|
|
/* Derive key for exactly one role index. Returns 0 on success, -1 on error. */
|
|
int crypto_derive_one(key_store_t *store, role_table_t *table, const mnemonic_state_t *mnemonic, int role_index);
|
|
|
|
/* Get the derived private key for a role (by table index). Returns NULL if not derived. */
|
|
const unsigned char *crypto_get_private_key(const key_store_t *store, int role_index);
|
|
|
|
/* Get the derived public key hex for a role. Returns NULL if not derived. */
|
|
const char *crypto_get_pubkey_hex(const key_store_t *store, int role_index);
|
|
|
|
/* Sign a Nostr event. event_json is the unsigned event JSON string.
|
|
* Returns a newly-allocated string containing the signed event JSON, or NULL on error.
|
|
* Caller must free() the returned string. */
|
|
char *crypto_sign_event(const key_store_t *store, int role_index, const char *event_json);
|
|
|
|
/* Zeroize all derived keys in the store. */
|
|
void crypto_wipe(key_store_t *store);
|
|
|
|
|
|
/* from dispatcher.h */
|
|
|
|
|
|
/* Dispatcher context — holds references to shared state */
|
|
typedef struct {
|
|
role_table_t *role_table;
|
|
mnemonic_state_t *mnemonic;
|
|
key_store_t *key_store;
|
|
} dispatcher_ctx_t;
|
|
|
|
/* Initialize dispatcher context */
|
|
void dispatcher_init(dispatcher_ctx_t *ctx, role_table_t *table, mnemonic_state_t *mnemonic, key_store_t *key_store);
|
|
|
|
/*
|
|
* Process a JSON-RPC request string and produce a JSON-RPC response string.
|
|
*
|
|
* The caller owns the returned string and must free() it.
|
|
* Returns NULL only on catastrophic allocation failure.
|
|
*
|
|
* Response format on success:
|
|
* { "id": "...", "result": "..." }
|
|
*
|
|
* Response format on error:
|
|
* { "id": "...", "error": { "code": <int>, "message": "..." } }
|
|
*
|
|
* Error codes:
|
|
* -32700 Parse error (invalid JSON)
|
|
* -32600 Invalid request (missing id/method/params)
|
|
* -32601 Method not found (unknown verb after enforcement)
|
|
* -32602 Invalid params
|
|
* 1001 ambiguous_role_selector
|
|
* 1002 role_not_found
|
|
* 1003 no_default_role
|
|
* 1004 purpose_mismatch
|
|
* 1005 curve_mismatch
|
|
* 1006 mnemonic_not_loaded
|
|
*/
|
|
char *dispatcher_handle_request(dispatcher_ctx_t *ctx, const char *json_request);
|
|
|
|
|
|
/* from server.h */
|
|
|
|
|
|
#define SERVER_SOCKET_NAME_MAX 108
|
|
#define SERVER_MAX_MSG_SIZE 65536
|
|
|
|
#define NSIGNER_LISTEN_UNIX 0
|
|
#define NSIGNER_LISTEN_STDIO 1
|
|
#define NSIGNER_LISTEN_QREXEC 2
|
|
#define NSIGNER_LISTEN_TCP 3
|
|
|
|
#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:<n>", "qubes:<vm>", or "pubkey:<hex>" */
|
|
char source_qube[64];
|
|
int auth_present;
|
|
char auth_pubkey_hex[65];
|
|
char auth_label[64];
|
|
} caller_identity_t;
|
|
|
|
/* Server context */
|
|
#define INDEX_WHITELIST_MAX 256 /* nostr_index range 0-255 */
|
|
#define INDEX_WHITELIST_BITMAP_SIZE (INDEX_WHITELIST_MAX / 8) /* 32 bytes */
|
|
|
|
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; /* when set, unix connections send a qrexec_source preamble */
|
|
int index_whitelist_active; /* 1 if index whitelist is set (not "all") */
|
|
unsigned char index_whitelist[INDEX_WHITELIST_BITMAP_SIZE]; /* bitmap of allowed nostr_index values */
|
|
} 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);
|
|
|
|
/* Mark this unix listener as a trusted bridge socket. When set, each
|
|
* incoming connection is expected to send a framed JSON preamble
|
|
* {"qrexec_source":"<vm>"} before the actual request, and the caller
|
|
* identity is composed as qubes:<vm>. */
|
|
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled);
|
|
|
|
/* Set the nostr_index whitelist from a parsed spec string.
|
|
* Spec format: "all" (no restriction), or comma-separated list of
|
|
* indices and ranges, e.g. "1,3,4" or "0-3" or "0-3,7,9".
|
|
* Returns 0 on success, -1 on parse error. */
|
|
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec);
|
|
|
|
/* Check if a nostr_index is in the whitelist. Returns 1 if allowed, 0 if not. */
|
|
int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index);
|
|
|
|
/* 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);
|
|
|
|
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_<word1>_<word2>
|
|
* Returns 0 on success, -1 on error.
|
|
*/
|
|
int socket_name_random(char *out, size_t out_len);
|
|
|
|
|
|
/* from main.h */
|
|
/*
|
|
* nsigner main header - version information
|
|
*
|
|
* Version macros are auto-updated by increment_and_push.sh.
|
|
*/
|
|
|
|
|
|
/* Version information (auto-updated by build/version tooling) */
|
|
#define NSIGNER_VERSION_MAJOR 0
|
|
#define NSIGNER_VERSION_MINOR 0
|
|
#define NSIGNER_VERSION_PATCH 2
|
|
#define NSIGNER_VERSION "v0.0.2"
|
|
|
|
|
|
/* NSIGNER_HEADERLESS_DECLS_END */
|
|
|
|
int transport_send_framed(int fd, const char *payload);
|
|
int transport_recv_framed(int fd, char **out_payload, size_t max_size);
|
|
|
|
#include "auth_envelope.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
static int g_prompt_always_allow = 0;
|
|
static int g_noninteractive_prompt_default = -1;
|
|
static auth_nonce_cache_t g_auth_nonce_cache;
|
|
static int g_auth_nonce_cache_inited = 0;
|
|
static server_approval_cb g_approval_cb = NULL;
|
|
static void *g_approval_cb_user_data = NULL;
|
|
|
|
static int caller_id_extract_ipv6(const char *caller_id, char *out_ipv6, size_t out_sz) {
|
|
const char *start;
|
|
const char *end;
|
|
size_t len;
|
|
|
|
if (caller_id == NULL || out_ipv6 == NULL || out_sz == 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (strncmp(caller_id, "tcp:[", 5) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
start = caller_id + 5;
|
|
end = strchr(start, ']');
|
|
if (end == NULL || end[1] != ':') {
|
|
return -1;
|
|
}
|
|
|
|
len = (size_t)(end - start);
|
|
if (len == 0 || len >= out_sz) {
|
|
return -1;
|
|
}
|
|
|
|
memcpy(out_ipv6, start, len);
|
|
out_ipv6[len] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static int read_cmd_output(const char *cmd, char **out_buf) {
|
|
FILE *fp;
|
|
char chunk[512];
|
|
char *buf = NULL;
|
|
size_t used = 0;
|
|
size_t cap = 0;
|
|
|
|
if (cmd == NULL || out_buf == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
*out_buf = NULL;
|
|
fp = popen(cmd, "r");
|
|
if (fp == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
while (fgets(chunk, sizeof(chunk), fp) != NULL) {
|
|
size_t n = strlen(chunk);
|
|
if (used + n + 1 > cap) {
|
|
size_t new_cap = (cap == 0) ? 2048 : cap * 2;
|
|
while (new_cap < used + n + 1) {
|
|
new_cap *= 2;
|
|
}
|
|
{
|
|
char *tmp = (char *)realloc(buf, new_cap);
|
|
if (tmp == NULL) {
|
|
free(buf);
|
|
(void)pclose(fp);
|
|
return -1;
|
|
}
|
|
buf = tmp;
|
|
cap = new_cap;
|
|
}
|
|
}
|
|
memcpy(buf + used, chunk, n);
|
|
used += n;
|
|
}
|
|
|
|
(void)pclose(fp);
|
|
|
|
if (buf == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
buf[used] = '\0';
|
|
*out_buf = buf;
|
|
return 0;
|
|
}
|
|
|
|
static int lookup_fips_peer_for_ipv6(const char *ipv6,
|
|
char *out_npub,
|
|
size_t out_npub_sz,
|
|
char *out_name,
|
|
size_t out_name_sz) {
|
|
char *json = NULL;
|
|
cJSON *root = NULL;
|
|
cJSON *peers = NULL;
|
|
int i;
|
|
|
|
if (ipv6 == NULL || out_npub == NULL || out_npub_sz == 0 || out_name == NULL || out_name_sz == 0) {
|
|
return -1;
|
|
}
|
|
|
|
out_npub[0] = '\0';
|
|
out_name[0] = '\0';
|
|
|
|
if (read_cmd_output("fipsctl show peers 2>/dev/null", &json) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
root = cJSON_Parse(json);
|
|
free(json);
|
|
if (root == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
peers = cJSON_GetObjectItemCaseSensitive(root, "peers");
|
|
if (!cJSON_IsArray(peers)) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; i < cJSON_GetArraySize(peers); ++i) {
|
|
cJSON *peer = cJSON_GetArrayItem(peers, i);
|
|
cJSON *peer_ip;
|
|
cJSON *peer_npub;
|
|
cJSON *peer_name;
|
|
|
|
if (!cJSON_IsObject(peer)) {
|
|
continue;
|
|
}
|
|
|
|
peer_ip = cJSON_GetObjectItemCaseSensitive(peer, "ipv6_addr");
|
|
if (!cJSON_IsString(peer_ip) || peer_ip->valuestring == NULL) {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(peer_ip->valuestring, ipv6) != 0) {
|
|
continue;
|
|
}
|
|
|
|
peer_npub = cJSON_GetObjectItemCaseSensitive(peer, "npub");
|
|
peer_name = cJSON_GetObjectItemCaseSensitive(peer, "display_name");
|
|
|
|
if (cJSON_IsString(peer_npub) && peer_npub->valuestring != NULL) {
|
|
strncpy(out_npub, peer_npub->valuestring, out_npub_sz - 1);
|
|
out_npub[out_npub_sz - 1] = '\0';
|
|
}
|
|
if (cJSON_IsString(peer_name) && peer_name->valuestring != NULL) {
|
|
strncpy(out_name, peer_name->valuestring, out_name_sz - 1);
|
|
out_name[out_name_sz - 1] = '\0';
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return (out_npub[0] != '\0') ? 0 : -1;
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
void server_set_prompt_always_allow(int enabled) {
|
|
g_prompt_always_allow = enabled ? 1 : 0;
|
|
}
|
|
|
|
void server_set_noninteractive_prompt_default(int decision) {
|
|
if (decision == POLICY_ALLOW || decision == POLICY_DENY) {
|
|
g_noninteractive_prompt_default = decision;
|
|
} else {
|
|
g_noninteractive_prompt_default = -1;
|
|
}
|
|
}
|
|
|
|
void server_set_approval_cb(server_approval_cb cb, void *user_data) {
|
|
g_approval_cb = cb;
|
|
g_approval_cb_user_data = user_data;
|
|
}
|
|
|
|
static int prompt_for_policy_decision(const caller_identity_t *caller,
|
|
const char *method,
|
|
const char *role_name,
|
|
const char *purpose,
|
|
int pending_derivation) {
|
|
int ch;
|
|
|
|
if (g_prompt_always_allow) {
|
|
return POLICY_ALLOW;
|
|
}
|
|
|
|
if (!isatty(STDIN_FILENO)) {
|
|
if (g_noninteractive_prompt_default == POLICY_ALLOW ||
|
|
g_noninteractive_prompt_default == POLICY_DENY) {
|
|
return g_noninteractive_prompt_default;
|
|
}
|
|
return POLICY_DENY;
|
|
}
|
|
|
|
if (g_approval_cb != NULL) {
|
|
server_approval_request_t req;
|
|
char ipv6[INET6_ADDRSTRLEN];
|
|
char npub[128];
|
|
char display_name[128];
|
|
|
|
req.caller = caller;
|
|
req.method = method;
|
|
req.role_name = role_name;
|
|
req.purpose = purpose;
|
|
req.pending_derivation = pending_derivation;
|
|
req.fips_peer_npub = NULL;
|
|
req.fips_peer_name = NULL;
|
|
|
|
npub[0] = '\0';
|
|
display_name[0] = '\0';
|
|
if (caller != NULL && caller->kind == NSIGNER_LISTEN_TCP &&
|
|
caller_id_extract_ipv6(caller->caller_id, ipv6, sizeof(ipv6)) == 0 &&
|
|
lookup_fips_peer_for_ipv6(ipv6, npub, sizeof(npub), display_name, sizeof(display_name)) == 0) {
|
|
req.fips_peer_npub = (npub[0] != '\0') ? npub : NULL;
|
|
req.fips_peer_name = (display_name[0] != '\0') ? display_name : NULL;
|
|
}
|
|
|
|
return g_approval_cb(&req, g_approval_cb_user_data);
|
|
}
|
|
|
|
printf("\nApproval required\n");
|
|
printf("caller: %s\n", (caller != NULL) ? caller->caller_id : "unknown");
|
|
if (caller != NULL && caller->kind == NSIGNER_LISTEN_TCP) {
|
|
char ipv6[INET6_ADDRSTRLEN];
|
|
char npub[128];
|
|
char display_name[128];
|
|
|
|
npub[0] = '\0';
|
|
display_name[0] = '\0';
|
|
|
|
if (caller_id_extract_ipv6(caller->caller_id, ipv6, sizeof(ipv6)) == 0 &&
|
|
lookup_fips_peer_for_ipv6(ipv6, npub, sizeof(npub), display_name, sizeof(display_name)) == 0) {
|
|
if (display_name[0] != '\0') {
|
|
printf("fips peer: %s (%s)\n", npub, display_name);
|
|
} else {
|
|
printf("fips peer: %s\n", npub);
|
|
}
|
|
}
|
|
}
|
|
|
|
printf("method: %s\n", (method != NULL) ? method : "unknown");
|
|
printf("role: %s\n", (role_name != NULL) ? role_name : "unknown");
|
|
printf("purpose: %s\n", (purpose != NULL) ? purpose : "unknown");
|
|
if (pending_derivation) {
|
|
printf(" ** NEW IDENTITY — will be derived if approved **\n");
|
|
}
|
|
printf("[y] allow once [n] deny [e] allow this caller+role+verb for session\n");
|
|
printf("[a] allow this caller+role for session (all verbs)\n> ");
|
|
fflush(stdout);
|
|
|
|
ch = getchar();
|
|
while (ch != EOF && ch != '\n') {
|
|
int next = getchar();
|
|
if (next == EOF || next == '\n') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
ch = tolower(ch);
|
|
if (ch == 'a') {
|
|
return POLICY_ALLOW_SESSION_ALL;
|
|
}
|
|
if (ch == 'e') {
|
|
return POLICY_ALLOW_SESSION_VERB;
|
|
}
|
|
if (ch == 'y') {
|
|
return POLICY_ALLOW;
|
|
}
|
|
return POLICY_DENY;
|
|
}
|
|
|
|
static void server_set_error(server_ctx_t *ctx, const char *msg) {
|
|
if (ctx == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (msg == NULL) {
|
|
ctx->last_error[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
strncpy(ctx->last_error, msg, sizeof(ctx->last_error) - 1);
|
|
ctx->last_error[sizeof(ctx->last_error) - 1] = '\0';
|
|
}
|
|
|
|
|
|
static int parse_tcp_target(const char *target,
|
|
int *out_family,
|
|
char *out_host,
|
|
size_t out_host_sz,
|
|
uint16_t *out_port) {
|
|
const char *p;
|
|
const char *host_start;
|
|
const char *host_end;
|
|
const char *port_start;
|
|
char port_buf[16];
|
|
size_t host_len;
|
|
size_t port_len;
|
|
char *endptr = NULL;
|
|
long port_long;
|
|
|
|
if (target == NULL || out_family == NULL || out_host == NULL || out_port == NULL ||
|
|
out_host_sz == 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (strncmp(target, "tcp:", 4) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
p = target + 4;
|
|
if (*p == '[') {
|
|
host_start = p + 1;
|
|
host_end = strchr(host_start, ']');
|
|
if (host_end == NULL || host_end[1] != ':') {
|
|
return -1;
|
|
}
|
|
port_start = host_end + 2;
|
|
} else {
|
|
host_start = p;
|
|
host_end = strrchr(p, ':');
|
|
if (host_end == NULL || host_end == host_start) {
|
|
return -1;
|
|
}
|
|
port_start = host_end + 1;
|
|
}
|
|
|
|
host_len = (size_t)(host_end - host_start);
|
|
if (host_len == 0 || host_len >= out_host_sz) {
|
|
return -1;
|
|
}
|
|
memcpy(out_host, host_start, host_len);
|
|
out_host[host_len] = '\0';
|
|
|
|
port_len = strlen(port_start);
|
|
if (port_len == 0 || port_len >= sizeof(port_buf)) {
|
|
return -1;
|
|
}
|
|
memcpy(port_buf, port_start, port_len + 1);
|
|
|
|
errno = 0;
|
|
port_long = strtol(port_buf, &endptr, 10);
|
|
if (errno != 0 || endptr == port_buf || *endptr != '\0' || port_long < 1 || port_long > 65535) {
|
|
return -1;
|
|
}
|
|
|
|
{
|
|
struct in6_addr addr6;
|
|
struct in_addr addr4;
|
|
|
|
if (inet_pton(AF_INET6, out_host, &addr6) == 1) {
|
|
*out_family = AF_INET6;
|
|
} else if (inet_pton(AF_INET, out_host, &addr4) == 1) {
|
|
*out_family = AF_INET;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
*out_port = (uint16_t)port_long;
|
|
return 0;
|
|
}
|
|
|
|
static void json_copy_string(char *dst, size_t dst_sz, const char *src, const char *fallback) {
|
|
const char *s = (src != NULL) ? src : fallback;
|
|
|
|
if (dst == NULL || dst_sz == 0) {
|
|
return;
|
|
}
|
|
|
|
if (s == NULL) {
|
|
dst[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
strncpy(dst, s, dst_sz - 1);
|
|
dst[dst_sz - 1] = '\0';
|
|
}
|
|
|
|
static int extract_request_id_compact(const char *json, char *out_id, size_t out_id_sz) {
|
|
cJSON *root;
|
|
cJSON *id_item;
|
|
char *printed = NULL;
|
|
|
|
if (json == NULL || out_id == NULL || out_id_sz == 0) {
|
|
return -1;
|
|
}
|
|
|
|
out_id[0] = '\0';
|
|
root = cJSON_Parse(json);
|
|
if (root == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
id_item = cJSON_GetObjectItemCaseSensitive(root, "id");
|
|
if (id_item == NULL) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
if (cJSON_IsString(id_item) && id_item->valuestring != NULL) {
|
|
json_copy_string(out_id, out_id_sz, id_item->valuestring, "null");
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
printed = cJSON_PrintUnformatted(id_item);
|
|
if (printed != NULL) {
|
|
json_copy_string(out_id, out_id_sz, printed, "null");
|
|
free(printed);
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
static int request_has_auth_field(const char *json) {
|
|
cJSON *root;
|
|
cJSON *auth;
|
|
int has_auth;
|
|
|
|
if (json == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
root = cJSON_Parse(json);
|
|
if (root == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
auth = cJSON_GetObjectItemCaseSensitive(root, "auth");
|
|
has_auth = (auth != NULL) ? 1 : 0;
|
|
cJSON_Delete(root);
|
|
return has_auth;
|
|
}
|
|
|
|
static void caller_id_compose_with_pubkey(char *out,
|
|
size_t out_sz,
|
|
int caller_kind,
|
|
const char *source_qube,
|
|
const char *pubkey_hex) {
|
|
if (out == NULL || out_sz == 0 || pubkey_hex == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (caller_kind == NSIGNER_LISTEN_QREXEC && source_qube != NULL && source_qube[0] != '\0') {
|
|
(void)snprintf(out, out_sz, "qubes:%s+pubkey:%s", source_qube, pubkey_hex);
|
|
return;
|
|
}
|
|
|
|
(void)snprintf(out, out_sz, "pubkey:%s", pubkey_hex);
|
|
}
|
|
|
|
static int extract_method_and_selector(const char *json,
|
|
char *method,
|
|
size_t method_sz,
|
|
selector_request_t *selector_req) {
|
|
cJSON *root;
|
|
cJSON *method_item;
|
|
cJSON *params_item;
|
|
cJSON *options_item;
|
|
cJSON *tmp;
|
|
|
|
if (json == NULL || method == NULL || selector_req == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
method[0] = '\0';
|
|
selector_request_init(selector_req);
|
|
|
|
root = cJSON_Parse(json);
|
|
if (root == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
method_item = cJSON_GetObjectItemCaseSensitive(root, "method");
|
|
if (!cJSON_IsString(method_item) || method_item->valuestring == NULL) {
|
|
cJSON_Delete(root);
|
|
return -1;
|
|
}
|
|
|
|
json_copy_string(method, method_sz, method_item->valuestring, "unknown");
|
|
|
|
params_item = cJSON_GetObjectItemCaseSensitive(root, "params");
|
|
if (cJSON_IsArray(params_item)) {
|
|
{
|
|
int params_count = cJSON_GetArraySize(params_item);
|
|
options_item = (params_count > 0) ? cJSON_GetArrayItem(params_item, params_count - 1) : NULL;
|
|
}
|
|
if (cJSON_IsObject(options_item)) {
|
|
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role");
|
|
if (cJSON_IsString(tmp) && tmp->valuestring != NULL) {
|
|
selector_req->has_role = 1;
|
|
json_copy_string(selector_req->role_name, sizeof(selector_req->role_name), tmp->valuestring, "");
|
|
}
|
|
|
|
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "nostr_index");
|
|
if (cJSON_IsNumber(tmp)) {
|
|
selector_req->has_nostr_index = 1;
|
|
selector_req->nostr_index = tmp->valueint;
|
|
}
|
|
|
|
tmp = cJSON_GetObjectItemCaseSensitive(options_item, "role_path");
|
|
if (cJSON_IsString(tmp) && tmp->valuestring != NULL) {
|
|
selector_req->has_role_path = 1;
|
|
json_copy_string(selector_req->role_path, sizeof(selector_req->role_path), tmp->valuestring, "");
|
|
}
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
return 0;
|
|
}
|
|
|
|
void server_init(server_ctx_t *ctx, const char *socket_name, int socket_name_explicit,
|
|
int listen_mode,
|
|
int auth_mode,
|
|
int auth_skew_seconds,
|
|
dispatcher_ctx_t *dispatcher, policy_table_t *policy) {
|
|
if (ctx == NULL) {
|
|
return;
|
|
}
|
|
|
|
memset(ctx, 0, sizeof(*ctx));
|
|
ctx->last_error[0] = '\0';
|
|
if (socket_name != NULL) {
|
|
strncpy(ctx->socket_name, socket_name, sizeof(ctx->socket_name) - 1);
|
|
ctx->socket_name[sizeof(ctx->socket_name) - 1] = '\0';
|
|
}
|
|
ctx->listen_fd = -1;
|
|
ctx->listen_mode = listen_mode;
|
|
ctx->stdio_handled = 0;
|
|
ctx->dispatcher = dispatcher;
|
|
ctx->policy = policy;
|
|
ctx->socket_name_explicit = socket_name_explicit ? 1 : 0;
|
|
ctx->auth_mode = auth_mode;
|
|
ctx->auth_skew_seconds = (auth_skew_seconds > 0) ? auth_skew_seconds : AUTH_DEFAULT_SKEW_SECONDS;
|
|
ctx->bridge_source_trusted = 0;
|
|
ctx->index_whitelist_active = 0;
|
|
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
|
|
if (!g_auth_nonce_cache_inited) {
|
|
auth_nonce_cache_init(&g_auth_nonce_cache);
|
|
g_auth_nonce_cache_inited = 1;
|
|
}
|
|
}
|
|
|
|
void server_set_bridge_source_trusted(server_ctx_t *ctx, int enabled) {
|
|
if (ctx == NULL) {
|
|
return;
|
|
}
|
|
ctx->bridge_source_trusted = enabled ? 1 : 0;
|
|
}
|
|
|
|
static void whitelist_set_bit(unsigned char *bitmap, int index) {
|
|
if (index >= 0 && index < INDEX_WHITELIST_MAX) {
|
|
bitmap[index / 8] |= (1 << (index % 8));
|
|
}
|
|
}
|
|
|
|
static int whitelist_get_bit(const unsigned char *bitmap, int index) {
|
|
if (index < 0 || index >= INDEX_WHITELIST_MAX) {
|
|
return 0;
|
|
}
|
|
return (bitmap[index / 8] >> (index % 8)) & 1;
|
|
}
|
|
|
|
int server_set_index_whitelist(server_ctx_t *ctx, const char *spec) {
|
|
char buf[256];
|
|
char *p;
|
|
char *token;
|
|
|
|
if (ctx == NULL || spec == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
/* "all" means no restriction */
|
|
if (strcmp(spec, "all") == 0) {
|
|
ctx->index_whitelist_active = 0;
|
|
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
|
|
return 0;
|
|
}
|
|
|
|
strncpy(buf, spec, sizeof(buf) - 1);
|
|
buf[sizeof(buf) - 1] = '\0';
|
|
|
|
/* Parse comma-separated entries: "1,3,4" or "0-3" or "0-3,7,9" */
|
|
memset(ctx->index_whitelist, 0, sizeof(ctx->index_whitelist));
|
|
p = buf;
|
|
while (p != NULL && *p != '\0') {
|
|
char *comma = strchr(p, ',');
|
|
if (comma != NULL) {
|
|
*comma = '\0';
|
|
}
|
|
|
|
/* Parse token: either "N" or "N-M" (range) */
|
|
char *dash = strchr(p, '-');
|
|
if (dash != NULL) {
|
|
*dash = '\0';
|
|
char *endptr1 = NULL, *endptr2 = NULL;
|
|
long lo = strtol(p, &endptr1, 10);
|
|
long hi = strtol(dash + 1, &endptr2, 10);
|
|
if (*endptr1 != '\0' || *endptr2 != '\0' || lo < 0 || hi < 0 ||
|
|
lo >= INDEX_WHITELIST_MAX || hi >= INDEX_WHITELIST_MAX || lo > hi) {
|
|
return -1;
|
|
}
|
|
for (long i = lo; i <= hi; i++) {
|
|
whitelist_set_bit(ctx->index_whitelist, (int)i);
|
|
}
|
|
} else {
|
|
char *endptr = NULL;
|
|
long idx = strtol(p, &endptr, 10);
|
|
if (*endptr != '\0' || idx < 0 || idx >= INDEX_WHITELIST_MAX) {
|
|
return -1;
|
|
}
|
|
whitelist_set_bit(ctx->index_whitelist, (int)idx);
|
|
}
|
|
|
|
p = (comma != NULL) ? comma + 1 : NULL;
|
|
}
|
|
|
|
ctx->index_whitelist_active = 1;
|
|
return 0;
|
|
}
|
|
|
|
int server_index_whitelist_allows(const server_ctx_t *ctx, int nostr_index) {
|
|
if (ctx == NULL) {
|
|
return 1; /* no ctx = allow (shouldn't happen) */
|
|
}
|
|
if (!ctx->index_whitelist_active) {
|
|
return 1; /* "all" mode = no restriction */
|
|
}
|
|
return whitelist_get_bit(ctx->index_whitelist, nostr_index);
|
|
}
|
|
|
|
int server_start(server_ctx_t *ctx) {
|
|
int fd;
|
|
struct sockaddr_un addr;
|
|
socklen_t addr_len;
|
|
int flags;
|
|
int attempt;
|
|
|
|
if (ctx == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
if (ctx->dispatcher == NULL || ctx->policy == NULL || ctx->socket_name[0] == '\0') {
|
|
server_set_error(ctx, "invalid server context (missing dispatcher/policy/socket_name)");
|
|
return -1;
|
|
}
|
|
|
|
server_set_error(ctx, NULL);
|
|
|
|
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
|
|
ctx->listen_fd = STDIN_FILENO;
|
|
ctx->running = 1;
|
|
ctx->stdio_handled = 0;
|
|
server_set_error(ctx, NULL);
|
|
return 0;
|
|
}
|
|
|
|
if (ctx->listen_mode == NSIGNER_LISTEN_TCP) {
|
|
int family;
|
|
uint16_t port;
|
|
char host[64];
|
|
int one = 1;
|
|
|
|
int prc = parse_tcp_target(ctx->socket_name, &family, host, sizeof(host), &port);
|
|
if (prc != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"invalid tcp listen target: %s (expected tcp:IPv4:PORT or tcp:[IPv6]:PORT)",
|
|
ctx->socket_name);
|
|
return -1;
|
|
}
|
|
|
|
fd = socket(family, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"socket(tcp) failed: %s",
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
|
|
|
if (family == AF_INET) {
|
|
struct sockaddr_in addr4;
|
|
memset(&addr4, 0, sizeof(addr4));
|
|
addr4.sin_family = AF_INET;
|
|
addr4.sin_port = htons(port);
|
|
if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) {
|
|
close(fd);
|
|
server_set_error(ctx, "inet_pton(AF_INET) failed for listen target");
|
|
return -1;
|
|
}
|
|
if (bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"bind(%s) failed: %s",
|
|
ctx->socket_name,
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
} else {
|
|
struct sockaddr_in6 addr6;
|
|
memset(&addr6, 0, sizeof(addr6));
|
|
addr6.sin6_family = AF_INET6;
|
|
addr6.sin6_port = htons(port);
|
|
if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) {
|
|
close(fd);
|
|
server_set_error(ctx, "inet_pton(AF_INET6) failed for listen target");
|
|
return -1;
|
|
}
|
|
if (bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)) != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"bind(%s) failed: %s",
|
|
ctx->socket_name,
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (listen(fd, 16) != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"listen() failed: %s",
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"fcntl(O_NONBLOCK) failed: %s",
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
ctx->listen_fd = fd;
|
|
ctx->running = 1;
|
|
server_set_error(ctx, NULL);
|
|
return 0;
|
|
}
|
|
|
|
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"socket(AF_UNIX, SOCK_STREAM) failed: %s",
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
for (attempt = 0; attempt < 8; ++attempt) {
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sun_family = AF_UNIX;
|
|
addr.sun_path[0] = '\0';
|
|
strncpy(&addr.sun_path[1], ctx->socket_name, sizeof(addr.sun_path) - 2);
|
|
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
|
|
|
|
addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(ctx->socket_name));
|
|
|
|
if (bind(fd, (struct sockaddr *)&addr, addr_len) == 0) {
|
|
break;
|
|
}
|
|
|
|
if (errno == EADDRINUSE && !ctx->socket_name_explicit) {
|
|
if (socket_name_random(ctx->socket_name, sizeof(ctx->socket_name)) == 0) {
|
|
continue;
|
|
}
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"bind(@%s) failed: %s (and failed to generate retry socket name)",
|
|
ctx->socket_name,
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
if (errno == EADDRINUSE && ctx->socket_name_explicit) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"bind(@%s) failed: %s (explicit --socket-name is already in use)",
|
|
ctx->socket_name,
|
|
strerror(errno));
|
|
} else {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"bind(@%s) failed: %s",
|
|
ctx->socket_name,
|
|
strerror(errno));
|
|
}
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
if (attempt >= 8) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"bind failed after retries: address already in use for generated names (try --socket-name)");
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
if (listen(fd, 5) != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"listen() failed: %s",
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
|
|
(void)snprintf(ctx->last_error,
|
|
sizeof(ctx->last_error),
|
|
"fcntl(O_NONBLOCK) failed: %s",
|
|
strerror(errno));
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
ctx->listen_fd = fd;
|
|
ctx->running = 1;
|
|
server_set_error(ctx, NULL);
|
|
return 0;
|
|
}
|
|
|
|
const char *server_last_error(const server_ctx_t *ctx) {
|
|
if (ctx == NULL || ctx->last_error[0] == '\0') {
|
|
return "unknown server error";
|
|
}
|
|
|
|
return ctx->last_error;
|
|
}
|
|
|
|
int server_get_caller(int fd, caller_identity_t *out) {
|
|
struct ucred cred;
|
|
socklen_t len = sizeof(cred);
|
|
|
|
if (out == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
if (fd == STDIN_FILENO) {
|
|
const char *src = getenv("QREXEC_REMOTE_DOMAIN");
|
|
out->kind = NSIGNER_LISTEN_STDIO;
|
|
if (src != NULL && src[0] != '\0') {
|
|
out->kind = NSIGNER_LISTEN_QREXEC;
|
|
strncpy(out->source_qube, src, sizeof(out->source_qube) - 1);
|
|
out->source_qube[sizeof(out->source_qube) - 1] = '\0';
|
|
(void)snprintf(out->caller_id, sizeof(out->caller_id), "qubes:%.57s", out->source_qube);
|
|
return 0;
|
|
}
|
|
|
|
out->uid = getuid();
|
|
out->gid = getgid();
|
|
out->pid = getpid();
|
|
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
|
|
return 0;
|
|
}
|
|
|
|
{
|
|
struct sockaddr_storage peer;
|
|
socklen_t peer_len = sizeof(peer);
|
|
if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) == 0) {
|
|
if (peer.ss_family == AF_INET) {
|
|
const struct sockaddr_in *in4 = (const struct sockaddr_in *)&peer;
|
|
char ip[INET_ADDRSTRLEN];
|
|
if (inet_ntop(AF_INET, &in4->sin_addr, ip, sizeof(ip)) != NULL) {
|
|
out->kind = NSIGNER_LISTEN_TCP;
|
|
(void)snprintf(out->caller_id,
|
|
sizeof(out->caller_id),
|
|
"tcp:%s:%u",
|
|
ip,
|
|
(unsigned int)ntohs(in4->sin_port));
|
|
return 0;
|
|
}
|
|
} else if (peer.ss_family == AF_INET6) {
|
|
const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&peer;
|
|
char ip6[INET6_ADDRSTRLEN];
|
|
if (inet_ntop(AF_INET6, &in6->sin6_addr, ip6, sizeof(ip6)) != NULL) {
|
|
out->kind = NSIGNER_LISTEN_TCP;
|
|
(void)snprintf(out->caller_id,
|
|
sizeof(out->caller_id),
|
|
"tcp:[%s]:%u",
|
|
ip6,
|
|
(unsigned int)ntohs(in6->sin6_port));
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
out->uid = cred.uid;
|
|
out->gid = cred.gid;
|
|
out->pid = cred.pid;
|
|
out->kind = NSIGNER_LISTEN_UNIX;
|
|
(void)snprintf(out->caller_id, sizeof(out->caller_id), "uid:%u", (unsigned int)out->uid);
|
|
return 0;
|
|
}
|
|
|
|
int server_handle_one(server_ctx_t *ctx, server_activity_cb cb, void *cb_data) {
|
|
int client_fd;
|
|
caller_identity_t caller;
|
|
char *request = NULL;
|
|
char *response = NULL;
|
|
char method[64];
|
|
char request_id[128];
|
|
char auth_pubkey[65];
|
|
char auth_label[64];
|
|
char role_name[ROLE_NAME_MAX];
|
|
char purpose[ROLE_PURPOSE_MAX];
|
|
selector_request_t selector_req;
|
|
role_entry_t *role = NULL;
|
|
int selector_rc;
|
|
int pchk;
|
|
policy_source_t policy_src = POLICY_SOURCE_DEFAULT;
|
|
int pending_derivation = 0;
|
|
int hard_selector_error = 0;
|
|
int derivation_error = 0;
|
|
char activity[256];
|
|
const char *verdict = "DENIED";
|
|
const char *source_label = "no-match";
|
|
int auth_err_code = 0;
|
|
const char *auth_err_msg = NULL;
|
|
|
|
if (ctx == NULL || ctx->listen_fd < 0 || ctx->dispatcher == NULL || ctx->policy == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
if (ctx->listen_mode == NSIGNER_LISTEN_STDIO || ctx->listen_mode == NSIGNER_LISTEN_QREXEC) {
|
|
if (ctx->stdio_handled) {
|
|
return 0;
|
|
}
|
|
client_fd = STDIN_FILENO;
|
|
ctx->stdio_handled = 1;
|
|
} else {
|
|
client_fd = accept(ctx->listen_fd, NULL, NULL);
|
|
if (client_fd < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (server_get_caller(client_fd, &caller) != 0) {
|
|
if (client_fd != STDIN_FILENO) {
|
|
close(client_fd);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Bridge-source-trusted path: when the unix listener is marked as a
|
|
* trusted bridge socket (started with --bridge-source-trusted), each
|
|
* connection sends a framed JSON preamble {"qrexec_source":"<vm>"}
|
|
* before the actual request. We parse it and override the caller
|
|
* identity to qubes:<vm>, matching the native qrexec identity path.
|
|
*
|
|
* Trust rationale: the bridge socket is an abstract unix socket
|
|
* inside the signer qube, reachable only by processes already inside
|
|
* that qube. The qrexec framework authenticated the source qube and
|
|
* set QREXEC_REMOTE_DOMAIN in the bridge's environment; the bridge
|
|
* relays that framework-vouched value to us.
|
|
*/
|
|
if (ctx->bridge_source_trusted &&
|
|
ctx->listen_mode == NSIGNER_LISTEN_UNIX &&
|
|
client_fd != STDIN_FILENO) {
|
|
char *preamble = NULL;
|
|
cJSON *proot = NULL;
|
|
cJSON *src_item;
|
|
|
|
if (transport_recv_framed(client_fd, &preamble, 4096) != 0) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"bridge_preamble_missing\"}}");
|
|
if (response != NULL) {
|
|
(void)transport_send_framed(client_fd, response);
|
|
free(response);
|
|
}
|
|
close(client_fd);
|
|
return 1;
|
|
}
|
|
|
|
proot = cJSON_Parse(preamble);
|
|
free(preamble);
|
|
|
|
if (proot == NULL) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"bridge_preamble_parse_error\"}}");
|
|
if (response != NULL) {
|
|
(void)transport_send_framed(client_fd, response);
|
|
free(response);
|
|
}
|
|
close(client_fd);
|
|
return 1;
|
|
}
|
|
|
|
src_item = cJSON_GetObjectItemCaseSensitive(proot, "qrexec_source");
|
|
if (cJSON_IsString(src_item) && src_item->valuestring != NULL &&
|
|
src_item->valuestring[0] != '\0') {
|
|
caller.kind = NSIGNER_LISTEN_QREXEC;
|
|
strncpy(caller.source_qube, src_item->valuestring, sizeof(caller.source_qube) - 1);
|
|
caller.source_qube[sizeof(caller.source_qube) - 1] = '\0';
|
|
(void)snprintf(caller.caller_id, sizeof(caller.caller_id),
|
|
"qubes:%.57s", caller.source_qube);
|
|
}
|
|
cJSON_Delete(proot);
|
|
}
|
|
|
|
if (transport_recv_framed(client_fd, &request, SERVER_MAX_MSG_SIZE) != 0) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32700,\"message\":\"parse_error\"}}");
|
|
if (response != NULL) {
|
|
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
|
|
free(response);
|
|
}
|
|
if (client_fd != STDIN_FILENO) {
|
|
close(client_fd);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
json_copy_string(method, sizeof(method), "unknown", "unknown");
|
|
json_copy_string(role_name, sizeof(role_name), "unknown", "unknown");
|
|
json_copy_string(purpose, sizeof(purpose), "unknown", "unknown");
|
|
json_copy_string(request_id, sizeof(request_id), "null", "null");
|
|
auth_pubkey[0] = '\0';
|
|
auth_label[0] = '\0';
|
|
|
|
{
|
|
int auth_required = 0;
|
|
int auth_optional = 0;
|
|
int has_auth_field = 0;
|
|
|
|
if (caller.kind == NSIGNER_LISTEN_TCP) {
|
|
auth_required = 1;
|
|
} else if (caller.kind == NSIGNER_LISTEN_QREXEC) {
|
|
if (ctx->auth_mode == NSIGNER_AUTH_REQUIRED) {
|
|
auth_required = 1;
|
|
} else if (ctx->auth_mode == NSIGNER_AUTH_OPTIONAL) {
|
|
auth_optional = 1;
|
|
}
|
|
}
|
|
|
|
has_auth_field = request_has_auth_field(request);
|
|
if (auth_required || (auth_optional && has_auth_field)) {
|
|
if (auth_envelope_verify_request(request,
|
|
&g_auth_nonce_cache,
|
|
ctx->auth_skew_seconds,
|
|
auth_pubkey,
|
|
sizeof(auth_pubkey),
|
|
auth_label,
|
|
sizeof(auth_label),
|
|
&auth_err_code,
|
|
&auth_err_msg) != 0) {
|
|
(void)extract_request_id_compact(request, request_id, sizeof(request_id));
|
|
if (auth_err_msg == NULL) {
|
|
auth_err_msg = "auth_envelope_malformed";
|
|
}
|
|
{
|
|
char errbuf[256];
|
|
(void)snprintf(errbuf,
|
|
sizeof(errbuf),
|
|
"{\"id\":\"%s\",\"error\":{\"code\":%d,\"message\":\"%s\"}}",
|
|
request_id,
|
|
(auth_err_code != 0) ? auth_err_code : AUTH_ERR_ENVELOPE_MALFORMED,
|
|
auth_err_msg);
|
|
response = strdup(errbuf);
|
|
}
|
|
if (response != NULL) {
|
|
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
|
|
free(response);
|
|
}
|
|
free(request);
|
|
if (client_fd != STDIN_FILENO) {
|
|
close(client_fd);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
caller.auth_present = 1;
|
|
json_copy_string(caller.auth_pubkey_hex,
|
|
sizeof(caller.auth_pubkey_hex),
|
|
auth_pubkey,
|
|
"");
|
|
json_copy_string(caller.auth_label,
|
|
sizeof(caller.auth_label),
|
|
auth_label,
|
|
"");
|
|
caller_id_compose_with_pubkey(caller.caller_id,
|
|
sizeof(caller.caller_id),
|
|
caller.kind,
|
|
caller.source_qube,
|
|
auth_pubkey);
|
|
}
|
|
}
|
|
|
|
if (extract_method_and_selector(request, method, sizeof(method), &selector_req) == 0) {
|
|
if (ctx->dispatcher->role_table != NULL) {
|
|
selector_rc = selector_resolve(&selector_req, ctx->dispatcher->role_table, &role);
|
|
if (selector_rc == SELECTOR_OK && role != NULL) {
|
|
json_copy_string(role_name, sizeof(role_name), role->name, "main");
|
|
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(role->purpose), "nostr");
|
|
} else if (selector_rc == SELECTOR_ERR_NOT_FOUND && selector_req.has_nostr_index) {
|
|
pending_derivation = 1;
|
|
if (selector_req.nostr_index == 0) {
|
|
json_copy_string(role_name, sizeof(role_name), "main", "main");
|
|
} else {
|
|
(void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index);
|
|
}
|
|
json_copy_string(purpose, sizeof(purpose), "nostr", "nostr");
|
|
} else if (selector_rc == SELECTOR_ERR_AMBIGUOUS ||
|
|
selector_rc == SELECTOR_ERR_NOT_FOUND ||
|
|
selector_rc == SELECTOR_ERR_NO_DEFAULT) {
|
|
hard_selector_error = selector_rc;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Index whitelist enforcement: if the request specifies a nostr_index
|
|
* and the whitelist is active (not "all"), check that the index is
|
|
* allowed. This is a hard deny — it happens before policy/approval,
|
|
* so even an approved caller cannot access a non-whitelisted index.
|
|
* We set hard_selector_error to a sentinel so the policy_check branch
|
|
* is skipped.
|
|
*/
|
|
if (hard_selector_error == 0 && selector_req.has_nostr_index &&
|
|
!server_index_whitelist_allows(ctx, selector_req.nostr_index)) {
|
|
(void)snprintf(role_name, sizeof(role_name), "nostr_idx_%d", selector_req.nostr_index);
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2002,\"message\":\"index_not_allowed\"}}");
|
|
pchk = POLICY_DENY;
|
|
verdict = "DENIED";
|
|
source_label = "index-not-approved";
|
|
hard_selector_error = -100; /* sentinel: skip policy_check */
|
|
}
|
|
|
|
if (hard_selector_error == SELECTOR_ERR_AMBIGUOUS) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1001,\"message\":\"ambiguous_role_selector\"}}");
|
|
pchk = POLICY_DENY;
|
|
} else if (hard_selector_error == SELECTOR_ERR_NO_DEFAULT) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1003,\"message\":\"no_default_role\"}}");
|
|
pchk = POLICY_DENY;
|
|
} else if (hard_selector_error == SELECTOR_ERR_NOT_FOUND) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":1002,\"message\":\"unknown_role\"}}");
|
|
pchk = POLICY_DENY;
|
|
} else if (hard_selector_error == 0) {
|
|
/* Normal path: run policy_check (skip if whitelist already denied) */
|
|
pchk = policy_check(ctx->policy, caller.caller_id, method, role_name, purpose, &policy_src);
|
|
}
|
|
/* else: hard_selector_error == -100 (whitelist deny) — keep pchk=POLICY_DENY */
|
|
|
|
if (pchk == POLICY_PROMPT) {
|
|
pchk = prompt_for_policy_decision(&caller, method, role_name, purpose, pending_derivation);
|
|
if (pchk == POLICY_ALLOW_SESSION_VERB || pchk == POLICY_ALLOW_SESSION_ALL) {
|
|
policy_entry_t grant;
|
|
|
|
memset(&grant, 0, sizeof(grant));
|
|
strncpy(grant.caller, caller.caller_id, sizeof(grant.caller) - 1);
|
|
strncpy(grant.roles[0], role_name, ROLE_NAME_MAX - 1);
|
|
grant.role_count = 1;
|
|
if (pchk == POLICY_ALLOW_SESSION_VERB) {
|
|
strncpy(grant.verbs[0], method, POLICY_VERB_MAX_LEN - 1);
|
|
grant.verb_count = 1;
|
|
}
|
|
grant.prompt = PROMPT_NEVER;
|
|
grant.source = POLICY_SOURCE_SESSION_GRANT;
|
|
if (policy_table_insert_before_last(ctx->policy, &grant) != 0) {
|
|
pchk = POLICY_DENY;
|
|
} else {
|
|
pchk = POLICY_ALLOW;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pchk == POLICY_ALLOW && pending_derivation) {
|
|
role_entry_t *new_role;
|
|
int role_index;
|
|
|
|
if (ctx->dispatcher == NULL ||
|
|
ctx->dispatcher->role_table == NULL ||
|
|
ctx->dispatcher->key_store == NULL ||
|
|
ctx->dispatcher->mnemonic == NULL ||
|
|
role_table_register_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index) != 0) {
|
|
derivation_error = 1;
|
|
} else {
|
|
new_role = role_table_find_by_nostr_index(ctx->dispatcher->role_table, selector_req.nostr_index);
|
|
if (new_role == NULL) {
|
|
derivation_error = 1;
|
|
} else {
|
|
role_index = (int)(new_role - &ctx->dispatcher->role_table->entries[0]);
|
|
if (role_index < 0 || role_index >= ctx->dispatcher->role_table->count ||
|
|
crypto_derive_one(ctx->dispatcher->key_store,
|
|
ctx->dispatcher->role_table,
|
|
ctx->dispatcher->mnemonic,
|
|
role_index) != 0) {
|
|
derivation_error = 1;
|
|
} else {
|
|
json_copy_string(role_name, sizeof(role_name), new_role->name, role_name);
|
|
json_copy_string(purpose, sizeof(purpose), role_purpose_to_str(new_role->purpose), "nostr");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pchk == POLICY_ALLOW && !derivation_error) {
|
|
verdict = "ALLOWED";
|
|
response = dispatcher_handle_request(ctx->dispatcher, request);
|
|
if (response == NULL) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}");
|
|
}
|
|
} else if (pchk == POLICY_ALLOW && derivation_error) {
|
|
verdict = "DENIED";
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":-32603,\"message\":\"internal_error\"}}");
|
|
} else {
|
|
verdict = "DENIED";
|
|
if (response == NULL) {
|
|
response = strdup("{\"id\":\"null\",\"error\":{\"code\":2001,\"message\":\"policy_denied\"}}");
|
|
}
|
|
}
|
|
|
|
if (response != NULL) {
|
|
(void)transport_send_framed((client_fd == STDIN_FILENO) ? STDOUT_FILENO : client_fd, response);
|
|
}
|
|
|
|
if (pchk == POLICY_ALLOW && policy_src == POLICY_SOURCE_PREAPPROVE) {
|
|
source_label = "preapprove";
|
|
} else if (pchk == POLICY_ALLOW && policy_src == POLICY_SOURCE_SESSION_GRANT) {
|
|
source_label = "session-grant";
|
|
} else if (pchk == POLICY_ALLOW) {
|
|
source_label = "prompt";
|
|
} else if (hard_selector_error == -100) {
|
|
/* Index whitelist denial — keep the label set by the whitelist check */
|
|
/* source_label already set to "index-not-approved" above */
|
|
} else {
|
|
source_label = "no-match";
|
|
}
|
|
|
|
(void)snprintf(activity,
|
|
sizeof(activity),
|
|
"%s %s(%s) %s:%s",
|
|
caller.caller_id,
|
|
method,
|
|
role_name,
|
|
verdict,
|
|
source_label);
|
|
|
|
if (cb != NULL) {
|
|
cb(activity, cb_data);
|
|
}
|
|
|
|
free(request);
|
|
free(response);
|
|
if (client_fd != STDIN_FILENO) {
|
|
close(client_fd);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void server_stop(server_ctx_t *ctx) {
|
|
if (ctx == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (ctx->listen_fd >= 0) {
|
|
if (ctx->listen_mode == NSIGNER_LISTEN_UNIX || ctx->listen_mode == NSIGNER_LISTEN_TCP) {
|
|
close(ctx->listen_fd);
|
|
}
|
|
ctx->listen_fd = -1;
|
|
}
|
|
ctx->running = 0;
|
|
}
|