v0.0.34 - Add qrexec bridge subcommand and --bridge-source-trusted flag for persistent-signer qrexec transport
This commit is contained in:
@@ -1,47 +1,44 @@
|
||||
# n_signer C Reference Client
|
||||
# n_signer C Client — migrated to `nostr_core_lib`
|
||||
|
||||
This directory provides a minimal copy/paste-friendly C client for `n_signer`.
|
||||
The hand-rolled `nsigner_client.{c,h}` that previously lived in this directory
|
||||
has been **removed**. n_signer now uses the shared, transport-pluggable client
|
||||
stack that lives in [`nostr_core_lib`](../resources/nostr_core_lib):
|
||||
|
||||
## Files
|
||||
- [`nostr_core/nsigner_transport.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_transport.h) —
|
||||
pluggable transport vtable (unix-abstract / tcp / usb-cdc serial / fds) + discovery.
|
||||
- [`nostr_core/nsigner_client.{h,c}`](../resources/nostr_core_lib/nostr_core/nsigner_client.h) —
|
||||
heap-allocated client: 4-byte length-prefixed framing, kind-27235 auth envelope
|
||||
(self-contained), JSON-RPC verbs, cJSON result parsing, RPC error mapping.
|
||||
- [`nostr_core/nostr_signer.{h,c}`](../resources/nostr_core_lib/nostr_core/nostr_signer.h) —
|
||||
high-level `nostr_signer_t` abstraction (local + remote backends, 6 verbs).
|
||||
|
||||
- `nsigner_client.h` / `nsigner_client.c`:
|
||||
- Unix abstract socket connect helper
|
||||
- length-prefixed frame send/receive
|
||||
- generic request API
|
||||
- helpers for `get_public_key` and `sign_event`
|
||||
- optional TCP auth envelope attachment via `auth_envelope_build_for_request()`
|
||||
This is the single source of truth for the n_signer wire contract. See
|
||||
[`nostr_core/NSIGNER_INTEGRATION.md`](../resources/nostr_core_lib/nostr_core/NSIGNER_INTEGRATION.md)
|
||||
for the full integration contract.
|
||||
|
||||
## API at a glance
|
||||
## What moved where
|
||||
|
||||
```c
|
||||
nsigner_client_t client;
|
||||
nsigner_client_init(&client);
|
||||
| Old (`client/`) | New (`nostr_core_lib`) |
|
||||
|---|---|
|
||||
| `nsigner_client_t` (stack) | `nsigner_client_t*` (heap) or `nostr_signer_t*` |
|
||||
| `nsigner_client_init` / `connect_unix` / `close` | `nsigner_transport_open_unix` + `nsigner_client_new` / `nsigner_client_free` |
|
||||
| `nsigner_client_get_public_key` | `nostr_signer_get_public_key` or `nsigner_client_call(..., "get_public_key", ...)` |
|
||||
| `nsigner_client_sign_event` | `nostr_signer_sign_event` or `nsigner_client_call(..., "sign_event", ...)` |
|
||||
| `nsigner_client_set_auth` | `nsigner_client_set_auth` or `nostr_signer_nsigner_set_auth` |
|
||||
| `nsigner_client_request` / `request_raw` | `nsigner_client_call` (returns parsed cJSON result) |
|
||||
|
||||
nsigner_client_connect_unix(&client, "nsigner", 5000);
|
||||
## Consumers (updated)
|
||||
|
||||
char *resp = NULL;
|
||||
nsigner_client_get_public_key(&client, "1", "", &resp);
|
||||
free(resp);
|
||||
- [`examples/get_public_key_client.c`](../examples/get_public_key_client.c) — uses `nsigner_transport_open_unix` + `nsigner_client_new` + `nsigner_client_call`.
|
||||
- [`examples/sign_event_client.c`](../examples/sign_event_client.c) — same pattern.
|
||||
- [`tests/test_integration.c`](../tests/test_integration.c) — same pattern; cJSON-based assertions.
|
||||
|
||||
nsigner_client_close(&client);
|
||||
```
|
||||
The `nsigner ... client '<json>'` subcommand in [`src/main.c`](../src/main.c) is
|
||||
unaffected — it has its own raw framing pass-through and never used this directory.
|
||||
|
||||
## Auth envelope usage
|
||||
## Why
|
||||
|
||||
If transport requires auth envelopes (for TCP mode), set a private key once:
|
||||
|
||||
```c
|
||||
unsigned char privkey[32] = { /* caller key */ };
|
||||
nsigner_client_set_auth(&client, privkey, "example-client");
|
||||
```
|
||||
|
||||
Subsequent requests automatically include an `auth` object.
|
||||
|
||||
## Ownership rules
|
||||
|
||||
- Any `out_response_json` returned by the API must be freed by caller using `free()`.
|
||||
- `nsigner_client_close()` only closes the socket; it does not free the client struct itself.
|
||||
|
||||
## License
|
||||
|
||||
Source files in this directory use SPDX `0BSD` headers for permissive reuse in external projects.
|
||||
Per [`plans/nsigner_integration_plan.md`](../resources/nostr_core_lib/plans/nsigner_integration_plan.md)
|
||||
(Phase 7): retire per-project hand-rolled clients in favor of the shared module
|
||||
in `nostr_core_lib`, so the wire contract has one implementation and downstream
|
||||
projects get unix/tcp/serial/fds transports for free.
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cJSON.h>
|
||||
|
||||
#include "auth_envelope.h"
|
||||
|
||||
#define NSIGNER_CLIENT_MAX_FRAME (65536U)
|
||||
|
||||
static void client_set_error(nsigner_client_t *client, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(client->last_error, sizeof(client->last_error), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static int write_full(int fd, const void *buf, size_t len) {
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, p + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_full(int fd, void *buf, size_t len) {
|
||||
unsigned char *p = (unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = read(fd, p + off, len - off);
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_framed(int fd, const char *payload) {
|
||||
uint32_t len;
|
||||
uint32_t be_len;
|
||||
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = (uint32_t)strlen(payload);
|
||||
be_len = htonl(len);
|
||||
|
||||
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_full(fd, payload, len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int recv_framed(int fd, char **out_payload) {
|
||||
uint32_t be_len;
|
||||
uint32_t len;
|
||||
char *payload;
|
||||
|
||||
if (out_payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_payload = NULL;
|
||||
|
||||
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = ntohl(be_len);
|
||||
if (len == 0 || len > NSIGNER_CLIENT_MAX_FRAME) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload = (char *)malloc((size_t)len + 1U);
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (read_full(fd, payload, len) != 0) {
|
||||
free(payload);
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload[len] = '\0';
|
||||
*out_payload = payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sleep_ms(int ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
|
||||
return nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
void nsigner_client_init(nsigner_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(client, 0, sizeof(*client));
|
||||
client->fd = -1;
|
||||
client->timeout_ms = 5000;
|
||||
}
|
||||
|
||||
void nsigner_client_close(nsigner_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->fd >= 0) {
|
||||
close(client->fd);
|
||||
client->fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
int nsigner_client_connect_unix(nsigner_client_t *client,
|
||||
const char *socket_name,
|
||||
int timeout_ms) {
|
||||
struct sockaddr_un addr;
|
||||
socklen_t addr_len;
|
||||
int elapsed = 0;
|
||||
|
||||
if (client == NULL || socket_name == NULL || socket_name[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
timeout_ms = (timeout_ms > 0) ? timeout_ms : 5000;
|
||||
nsigner_client_close(client);
|
||||
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
addr.sun_path[0] = '\0';
|
||||
strncpy(&addr.sun_path[1], 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(socket_name));
|
||||
|
||||
while (elapsed < timeout_ms) {
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
client_set_error(client, "socket failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
|
||||
client->fd = fd;
|
||||
client->timeout_ms = timeout_ms;
|
||||
client_set_error(client, "ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
sleep_ms(100);
|
||||
elapsed += 100;
|
||||
}
|
||||
|
||||
client_set_error(client, "connect timeout: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t *client,
|
||||
const unsigned char privkey[32],
|
||||
const char *label) {
|
||||
if (client == NULL || privkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(client->auth_privkey, privkey, 32);
|
||||
client->auth_enabled = 1;
|
||||
|
||||
if (label == NULL) {
|
||||
label = "";
|
||||
}
|
||||
strncpy(client->auth_label, label, sizeof(client->auth_label) - 1);
|
||||
client->auth_label[sizeof(client->auth_label) - 1] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *nsigner_client_last_error(const nsigner_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return "invalid_client";
|
||||
}
|
||||
return client->last_error;
|
||||
}
|
||||
|
||||
int nsigner_client_request_raw(nsigner_client_t *client,
|
||||
const char *request_json,
|
||||
char **out_response_json) {
|
||||
char *response_json = NULL;
|
||||
|
||||
if (client == NULL || request_json == NULL || out_response_json == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (client->fd < 0) {
|
||||
client_set_error(client, "not connected");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_response_json = NULL;
|
||||
|
||||
if (send_framed(client->fd, request_json) != 0) {
|
||||
client_set_error(client, "send failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (recv_framed(client->fd, &response_json) != 0) {
|
||||
client_set_error(client, "receive failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_response_json = response_json;
|
||||
client_set_error(client, "ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nsigner_client_request(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *method,
|
||||
const cJSON *params,
|
||||
char **out_response_json) {
|
||||
cJSON *root = NULL;
|
||||
cJSON *params_copy = NULL;
|
||||
cJSON *auth = NULL;
|
||||
char *request_json = NULL;
|
||||
int rc = -1;
|
||||
|
||||
if (client == NULL || id == NULL || method == NULL || out_response_json == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = cJSON_CreateObject();
|
||||
if (root == NULL) {
|
||||
client_set_error(client, "out of memory");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(root, "id", id);
|
||||
cJSON_AddStringToObject(root, "method", method);
|
||||
|
||||
if (params != NULL) {
|
||||
params_copy = cJSON_Duplicate((cJSON *)params, 1);
|
||||
} else {
|
||||
params_copy = cJSON_CreateArray();
|
||||
}
|
||||
|
||||
if (params_copy == NULL) {
|
||||
client_set_error(client, "failed to create params");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "params", params_copy);
|
||||
|
||||
if (client->auth_enabled) {
|
||||
if (auth_envelope_build_for_request(id,
|
||||
method,
|
||||
params_copy,
|
||||
client->auth_privkey,
|
||||
client->auth_label,
|
||||
time(NULL),
|
||||
&auth) != 0) {
|
||||
client_set_error(client, "failed to build auth envelope");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "auth", auth);
|
||||
auth = NULL;
|
||||
}
|
||||
|
||||
request_json = cJSON_PrintUnformatted(root);
|
||||
if (request_json == NULL) {
|
||||
client_set_error(client, "failed to serialize request");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = nsigner_client_request_raw(client, request_json, out_response_json);
|
||||
|
||||
cleanup:
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(auth);
|
||||
free(request_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nsigner_client_get_public_key(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *selector,
|
||||
char **out_response_json) {
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
int rc;
|
||||
|
||||
if (params == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString((selector != NULL) ? selector : ""));
|
||||
rc = nsigner_client_request(client, id, "get_public_key", params, out_response_json);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nsigner_client_sign_event(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *event_json,
|
||||
const char *role,
|
||||
char **out_response_json) {
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
cJSON *opts = cJSON_CreateObject();
|
||||
int rc;
|
||||
|
||||
if (params == NULL || opts == NULL || event_json == NULL) {
|
||||
cJSON_Delete(params);
|
||||
cJSON_Delete(opts);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
|
||||
if (role != NULL && role[0] != '\0') {
|
||||
cJSON_AddStringToObject(opts, "role", role);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
|
||||
rc = nsigner_client_request(client, id, "sign_event", params, out_response_json);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
*/
|
||||
|
||||
#ifndef NSIGNER_CLIENT_H
|
||||
#define NSIGNER_CLIENT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cJSON.h>
|
||||
|
||||
typedef struct {
|
||||
int fd;
|
||||
int timeout_ms;
|
||||
unsigned char auth_privkey[32];
|
||||
int auth_enabled;
|
||||
char auth_label[64];
|
||||
char last_error[128];
|
||||
} nsigner_client_t;
|
||||
|
||||
void nsigner_client_init(nsigner_client_t *client);
|
||||
void nsigner_client_close(nsigner_client_t *client);
|
||||
|
||||
int nsigner_client_connect_unix(nsigner_client_t *client,
|
||||
const char *socket_name,
|
||||
int timeout_ms);
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t *client,
|
||||
const unsigned char privkey[32],
|
||||
const char *label);
|
||||
|
||||
const char *nsigner_client_last_error(const nsigner_client_t *client);
|
||||
|
||||
int nsigner_client_request_raw(nsigner_client_t *client,
|
||||
const char *request_json,
|
||||
char **out_response_json);
|
||||
|
||||
int nsigner_client_request(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *method,
|
||||
const cJSON *params,
|
||||
char **out_response_json);
|
||||
|
||||
int nsigner_client_get_public_key(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *selector,
|
||||
char **out_response_json);
|
||||
|
||||
int nsigner_client_sign_event(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *event_json,
|
||||
const char *role,
|
||||
char **out_response_json);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user