Add qrexec transport, nostr_index selector, and index_not_allowed error code for n_signer client

This commit is contained in:
Laan Tungir
2026-07-11 14:25:25 -04:00
parent b76cff4a45
commit 5a9bd08b2e
5 changed files with 248 additions and 5 deletions
+3
View File
@@ -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
+54 -4
View File
@@ -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
+8
View File
@@ -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
+175 -1
View File
@@ -16,6 +16,7 @@
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <netdb.h>
#include <termios.h>
#include <unistd.h>
@@ -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];
+8
View File
@@ -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 <target_qube> <service_name>`
* 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);