Add qrexec transport, nostr_index selector, and index_not_allowed error code for n_signer client
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user