From 5a9bd08b2e64462cfc68eb54588165c080ffee0f Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sat, 11 Jul 2026 14:25:25 -0400 Subject: [PATCH] Add qrexec transport, nostr_index selector, and index_not_allowed error code for n_signer client --- nostr_core/nostr_common.h | 3 + nostr_core/nostr_signer.c | 58 ++++++++++- nostr_core/nostr_signer.h | 8 ++ nostr_core/nsigner_transport.c | 176 ++++++++++++++++++++++++++++++++- nostr_core/nsigner_transport.h | 8 ++ 5 files changed, 248 insertions(+), 5 deletions(-) diff --git a/nostr_core/nostr_common.h b/nostr_core/nostr_common.h index 89d9e20f..575ac4b3 100644 --- a/nostr_core/nostr_common.h +++ b/nostr_core/nostr_common.h @@ -28,6 +28,9 @@ #define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19 #define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20 #define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30 +/* nsigner remote backend errors */ +#define NOSTR_ERROR_NSIGNER_POLICY_DENIED -2001 +#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002 #define NOSTR_ERROR_EVENT_INVALID_ID -31 #define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32 #define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33 diff --git a/nostr_core/nostr_signer.c b/nostr_core/nostr_signer.c index 07e5f291..6e9c383f 100644 --- a/nostr_core/nostr_signer.c +++ b/nostr_core/nostr_signer.c @@ -30,6 +30,8 @@ struct nostr_signer { struct { nsigner_client_t* client; char role[64]; + int nostr_index; /* -1 = not set; when set, overrides role */ + int has_nostr_index; /* 1 if nostr_index is set */ } remote; #endif } u; @@ -284,7 +286,7 @@ static int signer_local_nip44_decrypt(nostr_signer_t* signer, #if defined(NOSTR_ENABLE_NSIGNER_CLIENT) -static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role) { +static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role, int has_nostr_index, int nostr_index) { cJSON* selector; if (params == NULL) { @@ -294,6 +296,18 @@ static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role } } + /* nostr_index takes precedence over role when set */ + if (has_nostr_index) { + selector = cJSON_CreateObject(); + if (selector == NULL) { + cJSON_Delete(params); + return NULL; + } + cJSON_AddNumberToObject(selector, "nostr_index", nostr_index); + cJSON_AddItemToArray(params, selector); + return params; + } + if (role == NULL || role[0] == '\0') { return params; } @@ -319,7 +333,9 @@ static int signer_remote_get_public_key(nostr_signer_t* signer, char out_pubkey_ return NOSTR_ERROR_INVALID_INPUT; } - params = signer_remote_params_with_selector(NULL, signer->u.remote.role); + params = signer_remote_params_with_selector(NULL, signer->u.remote.role, + signer->u.remote.has_nostr_index, + signer->u.remote.nostr_index); if (params == NULL) { return NOSTR_ERROR_MEMORY_FAILED; } @@ -369,7 +385,9 @@ static int signer_remote_sign_event(nostr_signer_t* signer, const cJSON* unsigne cJSON_AddItemToArray(params, cJSON_CreateString(event_json)); free(event_json); - params = signer_remote_params_with_selector(params, signer->u.remote.role); + params = signer_remote_params_with_selector(params, signer->u.remote.role, + signer->u.remote.has_nostr_index, + signer->u.remote.nostr_index); if (params == NULL) { return NOSTR_ERROR_MEMORY_FAILED; } @@ -427,7 +445,9 @@ static int signer_remote_encrypt_decrypt(nostr_signer_t* signer, cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey_hex)); cJSON_AddItemToArray(params, cJSON_CreateString(in)); - params = signer_remote_params_with_selector(params, signer->u.remote.role); + params = signer_remote_params_with_selector(params, signer->u.remote.role, + signer->u.remote.has_nostr_index, + signer->u.remote.nostr_index); if (params == NULL) { return NOSTR_ERROR_MEMORY_FAILED; } @@ -720,4 +740,34 @@ int nostr_signer_nsigner_set_auth(nostr_signer_t* signer, return nsigner_client_set_auth(signer->u.remote.client, auth_privkey, label); } + +nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms) { + nsigner_transport_t* transport; + + transport = nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms); + return nostr_signer_nsigner_from_transport(transport, role); +} + +int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index) { + if (signer == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (signer->backend != NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE || signer->u.remote.client == NULL) { + return NOSTR_ERROR_INVALID_INPUT; + } + + if (nostr_index < 0) { + /* Clear the nostr_index selector, fall back to role */ + signer->u.remote.has_nostr_index = 0; + signer->u.remote.nostr_index = -1; + } else { + /* Set nostr_index; clear role to avoid ambiguous selector */ + signer->u.remote.has_nostr_index = 1; + signer->u.remote.nostr_index = nostr_index; + memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role)); + } + + return NOSTR_SUCCESS; +} #endif diff --git a/nostr_core/nostr_signer.h b/nostr_core/nostr_signer.h index fc255f7c..c46e9ba8 100644 --- a/nostr_core/nostr_signer.h +++ b/nostr_core/nostr_signer.h @@ -42,10 +42,18 @@ nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* r nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms); nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms); nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms); +nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms); /* TCP mode requires auth envelope per n_signer; set this before making calls. */ int nostr_signer_nsigner_set_auth(nostr_signer_t* signer, const unsigned char auth_privkey[32], const char* label); +/* + * Set the nostr_index selector for the remote nsigner backend. When set, + * requests use {"nostr_index":N} instead of {"role":"..."}. This is mutually + * exclusive with the role parameter (setting index clears role). + * Returns NOSTR_SUCCESS or an error code. + */ +int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index); #endif #ifdef __cplusplus diff --git a/nostr_core/nsigner_transport.c b/nostr_core/nsigner_transport.c index 3a65c91a..df37ce6c 100644 --- a/nostr_core/nsigner_transport.c +++ b/nostr_core/nsigner_transport.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,8 @@ typedef enum { NSIGNER_TRANSPORT_KIND_FD = 0, NSIGNER_TRANSPORT_KIND_UNIX = 1, NSIGNER_TRANSPORT_KIND_TCP = 2, - NSIGNER_TRANSPORT_KIND_SERIAL = 3 + NSIGNER_TRANSPORT_KIND_SERIAL = 3, + NSIGNER_TRANSPORT_KIND_QREXEC = 4 } nsigner_transport_kind_t; /* Forward declarations: reconnect helpers use these before their definitions. */ @@ -54,6 +56,10 @@ typedef struct { char host[256]; /* tcp: host */ int port; /* tcp: port */ char device_path[256]; /* serial: device path */ + /* qrexec: target qube and service name */ + char qrexec_target[128]; /* qrexec: target qube name */ + char qrexec_service[128];/* qrexec: service name (e.g. "qubes.NsignerRpc") */ + pid_t qrexec_pid; /* qrexec: child process pid (0 = none) */ } nsigner_fd_transport_ctx_t; static int nsigner_write_full(int fd, const void* buf, size_t len) { @@ -349,6 +355,114 @@ static int nsigner_fd_reconnect_serial(nsigner_fd_transport_ctx_t* ctx) { return 0; } +/* Forward declaration */ +static int nsigner_fd_reconnect_qrexec(nsigner_fd_transport_ctx_t* ctx); + +/* Lazy connect for qrexec: if no fds are open, spawn the process now. */ +static int nsigner_qrexec_ensure_connected(nsigner_fd_transport_ctx_t* ctx) { + if (ctx == NULL) { + return -1; + } + if (ctx->read_fd >= 0 && ctx->write_fd >= 0) { + return 0; /* already connected */ + } + return nsigner_fd_reconnect_qrexec(ctx); +} + +static int nsigner_qrexec_send_framed(nsigner_transport_t* t, const char* json, size_t len) { + nsigner_fd_transport_ctx_t* ctx; + if (t == NULL || t->ctx == NULL) { + return -1; + } + ctx = (nsigner_fd_transport_ctx_t*)t->ctx; + if (nsigner_qrexec_ensure_connected(ctx) != 0) { + return -1; + } + return nsigner_fd_send_framed(t, json, len); +} + +static int nsigner_qrexec_recv_framed(nsigner_transport_t* t, char** out_json, size_t* out_len) { + nsigner_fd_transport_ctx_t* ctx; + if (t == NULL || t->ctx == NULL) { + return -1; + } + ctx = (nsigner_fd_transport_ctx_t*)t->ctx; + if (nsigner_qrexec_ensure_connected(ctx) != 0) { + return -1; + } + return nsigner_fd_recv_framed(t, out_json, out_len); +} + +static int nsigner_fd_reconnect_qrexec(nsigner_fd_transport_ctx_t* ctx) { + int stdin_pipe[2] = { -1, -1 }; + int stdout_pipe[2] = { -1, -1 }; + pid_t pid; + + if (ctx == NULL) { + return -1; + } + + /* Reap any previous child */ + if (ctx->qrexec_pid > 0) { + int status; + (void)waitpid(ctx->qrexec_pid, &status, WNOHANG); + ctx->qrexec_pid = 0; + } + + /* Create pipes: stdin_pipe[1] = we write, stdin_pipe[0] = child reads + * stdout_pipe[0] = we read, stdout_pipe[1] = child writes */ + if (pipe(stdin_pipe) != 0) { + return -1; + } + if (pipe(stdout_pipe) != 0) { + close(stdin_pipe[0]); close(stdin_pipe[1]); + return -1; + } + + pid = fork(); + if (pid < 0) { + close(stdin_pipe[0]); close(stdin_pipe[1]); + close(stdout_pipe[0]); close(stdout_pipe[1]); + return -1; + } + + if (pid == 0) { + /* Child: dup pipes onto stdin/stdout, exec qrexec-client-vm */ + (void)close(stdin_pipe[1]); + (void)close(stdout_pipe[0]); + if (dup2(stdin_pipe[0], STDIN_FILENO) < 0) { + _exit(127); + } + if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0) { + _exit(127); + } + (void)close(stdin_pipe[0]); + (void)close(stdout_pipe[1]); + /* Redirect stderr to /dev/null so it doesn't corrupt our pipe */ + { + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + (void)dup2(devnull, STDERR_FILENO); + (void)close(devnull); + } + } + (void)execlp("qrexec-client-vm", "qrexec-client-vm", + ctx->qrexec_target, ctx->qrexec_service, (char*)NULL); + _exit(127); + } + + /* Parent */ + (void)close(stdin_pipe[0]); + (void)close(stdout_pipe[1]); + + ctx->read_fd = stdout_pipe[0]; + ctx->write_fd = stdin_pipe[1]; + ctx->close_read_fd = 1; + ctx->close_write_fd = 1; + ctx->qrexec_pid = pid; + return 0; +} + static int nsigner_fd_reconnect(nsigner_transport_t* t) { nsigner_fd_transport_ctx_t* ctx; int rc; @@ -372,6 +486,9 @@ static int nsigner_fd_reconnect(nsigner_transport_t* t) { case NSIGNER_TRANSPORT_KIND_SERIAL: rc = nsigner_fd_reconnect_serial(ctx); break; + case NSIGNER_TRANSPORT_KIND_QREXEC: + rc = nsigner_fd_reconnect_qrexec(ctx); + break; default: /* FD-pair transport does not support reconnection. */ return -1; @@ -390,6 +507,12 @@ static void nsigner_fd_close(nsigner_transport_t* t) { ctx = (nsigner_fd_transport_ctx_t*)t->ctx; if (ctx != NULL) { nsigner_fd_close_fds(ctx); + /* Reap qrexec child process if any */ + if (ctx->qrexec_pid > 0) { + int status; + (void)waitpid(ctx->qrexec_pid, &status, 0); + ctx->qrexec_pid = 0; + } free(ctx); } @@ -763,6 +886,57 @@ nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int t return t; } +nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms) { + nsigner_transport_t* t = NULL; + nsigner_fd_transport_ctx_t* ctx = NULL; + + if (target_qube == NULL || target_qube[0] == '\0' || + service_name == NULL || service_name[0] == '\0') { + return NULL; + } + + if (timeout_ms <= 0) { + timeout_ms = 30000; /* qrexec may involve dom0 policy prompts */ + } + + t = (nsigner_transport_t*)calloc(1, sizeof(*t)); + if (t == NULL) { + return NULL; + } + + ctx = (nsigner_fd_transport_ctx_t*)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + free(t); + return NULL; + } + + /* Initialize ctx fields directly (no initial fds — reconnect spawns the process) */ + ctx->kind = NSIGNER_TRANSPORT_KIND_QREXEC; + ctx->read_fd = -1; + ctx->write_fd = -1; + ctx->timeout_ms = timeout_ms; + ctx->close_read_fd = 0; + ctx->close_write_fd = 0; + ctx->qrexec_pid = 0; + t->ctx = ctx; + t->send_framed = nsigner_qrexec_send_framed; + t->recv_framed = nsigner_qrexec_recv_framed; + t->reconnect = nsigner_fd_reconnect; + t->close = nsigner_fd_close; + + /* Set qrexec parameters */ + strncpy(ctx->qrexec_target, target_qube, sizeof(ctx->qrexec_target) - 1); + ctx->qrexec_target[sizeof(ctx->qrexec_target) - 1] = '\0'; + strncpy(ctx->qrexec_service, service_name, sizeof(ctx->qrexec_service) - 1); + ctx->qrexec_service[sizeof(ctx->qrexec_service) - 1] = '\0'; + + /* No initial connect — qrexec-client-vm is spawned on the first + * reconnect (which nsigner_client_call does before each request). + * Spawning here would block waiting for a dom0 policy prompt that + * may never come if the caller hasn't sent a request yet. */ + return t; +} + int nsigner_transport_list_unix(char names[][64], int max_names) { FILE* fp; char line[512]; diff --git a/nostr_core/nsigner_transport.h b/nostr_core/nsigner_transport.h index 5c621498..fcf2e11d 100644 --- a/nostr_core/nsigner_transport.h +++ b/nostr_core/nsigner_transport.h @@ -46,6 +46,14 @@ nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int */ nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms); +/* + * Qubes qrexec transport: spawns `qrexec-client-vm ` + * as a subprocess and pipes framed I/O through its stdin/stdout. Each reconnect + * re-spawns the subprocess (qrexec handles one request per invocation). + * Only works on Qubes OS AppVMs; on other hosts the exec will fail. + */ +nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms); + /* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */ int nsigner_transport_list_unix(char names[][64], int max_names);