v0.0.42 - Add C qrexec client example using high-level nostr_signer API with nostr_index selector; sync nostr_core_lib with qrexec transport and index support
This commit is contained in:
7
Makefile
7
Makefile
@@ -43,6 +43,7 @@ TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
|
|||||||
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
||||||
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
||||||
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
|
EXAMPLE_GET_PUBKEY_TCP_TARGET := $(BUILD_DIR)/example_get_pubkey_tcp
|
||||||
|
EXAMPLE_GET_PUBKEY_QREXEC_TARGET := $(BUILD_DIR)/example_get_pubkey_qrexec
|
||||||
|
|
||||||
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth examples test-client clean
|
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth examples test-client clean
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
|||||||
|
|
||||||
test-client: examples
|
test-client: examples
|
||||||
|
|
||||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET)
|
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET) $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET)
|
||||||
|
|
||||||
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@@ -167,5 +168,9 @@ $(EXAMPLE_GET_PUBKEY_TCP_TARGET): $(EXAMPLES_DIR)/get_pubkey_tcp.c
|
|||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_tcp.c -o $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(LDFLAGS)
|
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_tcp.c -o $(EXAMPLE_GET_PUBKEY_TCP_TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
|
$(EXAMPLE_GET_PUBKEY_QREXEC_TARGET): $(EXAMPLES_DIR)/get_pubkey_qrexec.c
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
$(CC) $(CFLAGS) $(EXAMPLES_DIR)/get_pubkey_qrexec.c -o $(EXAMPLE_GET_PUBKEY_QREXEC_TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
|||||||
110
examples/get_pubkey_qrexec.c
Normal file
110
examples/get_pubkey_qrexec.c
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* get_pubkey_qrexec.c — connect to a running n_signer in another Qubes qube
|
||||||
|
* via qrexec, using the high-level nostr_signer API from nostr_core_lib.
|
||||||
|
*
|
||||||
|
* This demonstrates the new nostr_core_lib client features:
|
||||||
|
* - nostr_signer_nsigner_qrexec() — qrexec transport
|
||||||
|
* - nostr_signer_nsigner_set_nostr_index() — index-based key selection
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ./get_pubkey_qrexec <target_qube> [nostr_index]
|
||||||
|
* ./get_pubkey_qrexec nostr_signer 0
|
||||||
|
* ./get_pubkey_qrexec nostr_signer 1
|
||||||
|
*
|
||||||
|
* No auth envelope needed — qrexec identity comes from QREXEC_REMOTE_DOMAIN
|
||||||
|
* on the server side.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "nostr_common.h"
|
||||||
|
#include "nostr_signer.h"
|
||||||
|
#include "nip019.h"
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *target_qube;
|
||||||
|
const char *service_name = "qubes.NsignerRpc";
|
||||||
|
int nostr_index = 0;
|
||||||
|
nostr_signer_t *signer = NULL;
|
||||||
|
char pubkey_hex[65];
|
||||||
|
unsigned char pubkey_bytes[32];
|
||||||
|
char npub[128];
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <target_qube> [nostr_index]\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
target_qube = argv[1];
|
||||||
|
if (argc > 2) {
|
||||||
|
nostr_index = atoi(argv[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nostr_init() != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "failed to initialize crypto subsystem\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Connecting to n_signer in qube \"%s\" via qrexec (index %d)...\n",
|
||||||
|
target_qube, nostr_index);
|
||||||
|
|
||||||
|
/* Create a high-level signer backed by qrexec transport */
|
||||||
|
signer = nostr_signer_nsigner_qrexec(target_qube, service_name, NULL, 30000);
|
||||||
|
if (signer == NULL) {
|
||||||
|
fprintf(stderr, "failed to create qrexec signer (is qrexec-client-vm available?)\n");
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select key by nostr_index (NIP-06 m/44'/1237'/N'/0/0) */
|
||||||
|
if (nostr_signer_nsigner_set_nostr_index(signer, nostr_index) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "failed to set nostr_index\n");
|
||||||
|
nostr_signer_free(signer);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Request the public key */
|
||||||
|
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||||
|
if (rc != NOSTR_SUCCESS) {
|
||||||
|
if (rc == NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED) {
|
||||||
|
fprintf(stderr, "DENIED: index %d is not in the signer's whitelist\n", nostr_index);
|
||||||
|
} else if (rc == NOSTR_ERROR_NSIGNER_POLICY_DENIED) {
|
||||||
|
fprintf(stderr, "DENIED: policy denied (caller not approved at signer terminal)\n");
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "get_public_key failed: error code %d\n", rc);
|
||||||
|
}
|
||||||
|
nostr_signer_free(signer);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert hex pubkey to npub (bech32) */
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 32; i++) {
|
||||||
|
unsigned int byte;
|
||||||
|
if (sscanf(pubkey_hex + 2 * i, "%2x", &byte) != 1) {
|
||||||
|
fprintf(stderr, "failed to parse hex pubkey\n");
|
||||||
|
nostr_signer_free(signer);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
pubkey_bytes[i] = (unsigned char)byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nostr_key_to_bech32(pubkey_bytes, "npub", npub) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "failed to convert to npub\n");
|
||||||
|
nostr_signer_free(signer);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("index %d: hex=%s npub=%s\n", nostr_index, pubkey_hex, npub);
|
||||||
|
|
||||||
|
nostr_signer_free(signer);
|
||||||
|
nostr_cleanup();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
123
plans/nostr_core_lib_client_updates.md
Normal file
123
plans/nostr_core_lib_client_updates.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Plan: nostr_core_lib client updates for new n_signer features
|
||||||
|
|
||||||
|
Status: design / ready for review.
|
||||||
|
|
||||||
|
Related:
|
||||||
|
- [`../nostr_core_lib/nostr_core/nsigner_transport.h`](../../nostr_core_lib/nostr_core/nsigner_transport.h) — transport vtable
|
||||||
|
- [`../nostr_core_lib/nostr_core/nsigner_client.h`](../../nostr_core_lib/nostr_core/nsigner_client.h) — low-level client
|
||||||
|
- [`../nostr_core_lib/nostr_core/nostr_signer.h`](../../nostr_core_lib/nostr_core/nostr_signer.h) — high-level signer
|
||||||
|
- [`../nostr_core_lib/nostr_core/nostr_signer.c`](../../nostr_core_lib/nostr_core/nostr_signer.c) — `signer_remote_params_with_selector`
|
||||||
|
- [`plans/qrexec_persistent_bridge.md`](qrexec_persistent_bridge.md) — qrexec bridge design
|
||||||
|
- [`plans/interactive_transport_selection.md`](interactive_transport_selection.md) — index whitelist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Gaps identified
|
||||||
|
|
||||||
|
The `nostr_core_lib` client stack was built before the recent n_signer features. Three gaps need filling:
|
||||||
|
|
||||||
|
### 1.1 No `nostr_index` selector in the high-level API
|
||||||
|
|
||||||
|
`signer_remote_params_with_selector()` ([`nostr_signer.c:287`](../../nostr_core_lib/nostr_core/nostr_signer.c:287)) only emits `{"role":"..."}`. It does not support `{"nostr_index":N}`, which is n_signer's primary key-selection mechanism for Nostr identities (NIP-06 `m/44'/1237'/N'/0/0`).
|
||||||
|
|
||||||
|
The `nostr_signer_nsigner_*` constructors take a `const char* role` parameter. There's no way to say "use nostr_index 3" through the high-level API. Callers who want a specific Nostr identity by index must drop down to the low-level `nsigner_client_call` and build params manually — which is exactly what [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) and [`examples/n_signer_qube_example_fips.js`](../examples/n_signer_qube_example_fips.js) do.
|
||||||
|
|
||||||
|
### 1.2 No qrexec transport
|
||||||
|
|
||||||
|
`nsigner_transport_open_*` supports unix, tcp, serial, fds — but not qrexec. On Qubes OS, the primary inter-qube path is `qrexec-client-vm <target> qubes.NsignerRpc`, which pipes framed I/O through stdin/stdout of a subprocess. There's no transport that wraps this.
|
||||||
|
|
||||||
|
### 1.3 No error code mapping for `index_not_allowed` (2002)
|
||||||
|
|
||||||
|
n_signer now returns `{"error":{"code":2002,"message":"index_not_allowed"}}` when the index whitelist denies a request. The client should surface this distinctly from `policy_denied` (2001) so callers can tell "this index isn't whitelisted" vs "this caller isn't approved".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Proposed changes
|
||||||
|
|
||||||
|
### 2.1 Add `nostr_index` selector to the high-level API
|
||||||
|
|
||||||
|
**Option A (minimal):** Add a new constructor variant that takes `nostr_index` instead of `role`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
nostr_signer_t* nostr_signer_nsigner_unix_index(const char* socket_name, int nostr_index, int timeout_ms);
|
||||||
|
nostr_signer_t* nostr_signer_nsigner_tcp_index(const char* host, int port, int nostr_index, int timeout_ms);
|
||||||
|
/* etc. */
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B (cleaner):** Replace the `role` string parameter with a selector struct:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
const char* role; /* NULL = use default */
|
||||||
|
int nostr_index; /* -1 = not set */
|
||||||
|
const char* role_path; /* NULL = not set */
|
||||||
|
} nsigner_selector_t;
|
||||||
|
|
||||||
|
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const nsigner_selector_t* selector, int timeout_ms);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C (additive, chosen):** Keep existing constructors, add a setter for nostr_index:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
|
||||||
|
```
|
||||||
|
|
||||||
|
This sets the index on the remote backend, and `signer_remote_params_with_selector` emits `{"nostr_index":N}` instead of `{"role":"..."}` when set. Existing callers using `role` are unaffected.
|
||||||
|
|
||||||
|
**Decision: Option C** — additive, no API break, smallest change.
|
||||||
|
|
||||||
|
### 2.2 Add qrexec transport
|
||||||
|
|
||||||
|
New transport constructor:
|
||||||
|
|
||||||
|
```c
|
||||||
|
nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms);
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation: `fork()` + `execvp("qrexec-client-vm", [target_qube, service_name])` with `stdin`/`stdout` pipes. The `send_framed`/`recv_framed` vtable methods write/read through the pipes. `reconnect` re-spawns the subprocess (qrexec handles one request per invocation, so reconnect is mandatory before each call — same as the existing per-request reconnect pattern).
|
||||||
|
|
||||||
|
The transport is stateless per call: each `reconnect` spawns a fresh `qrexec-client-vm` process, sends one framed request, reads one framed response, and the process exits. This matches the qrexec execution model.
|
||||||
|
|
||||||
|
### 2.3 Add `nostr_signer_nsigner_qrexec` high-level constructor
|
||||||
|
|
||||||
|
```c
|
||||||
|
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms);
|
||||||
|
```
|
||||||
|
|
||||||
|
Wraps `nsigner_transport_open_qrexec` + `nostr_signer_nsigner_from_transport`. No auth envelope needed — qrexec identity comes from `QREXEC_REMOTE_DOMAIN` on the server side.
|
||||||
|
|
||||||
|
### 2.4 Surface `index_not_allowed` error distinctly
|
||||||
|
|
||||||
|
Add to `nostr_common.h`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
|
||||||
|
```
|
||||||
|
|
||||||
|
In `nsigner_client_call` (or `nostr_signer` remote backend), when the response error code is 2002, map it to `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` so callers can distinguish it from generic policy denial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementation checklist
|
||||||
|
|
||||||
|
In `nostr_core_lib`:
|
||||||
|
|
||||||
|
- [ ] `nsigner_transport.h` / `nsigner_transport.c`: add `nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms)` — fork+exec `qrexec-client-vm`, pipe-based framed I/O, reconnect re-spawns.
|
||||||
|
- [ ] `nostr_signer.h` / `nostr_signer.c`: add `nostr_signer_nsigner_qrexec(target_qube, service_name, role, timeout_ms)`.
|
||||||
|
- [ ] `nostr_signer.c`: add `nostr_signer_nsigner_set_nostr_index(signer, nostr_index)`; update `signer_remote_params_with_selector` to emit `{"nostr_index":N}` when index is set.
|
||||||
|
- [ ] `nostr_common.h`: add `NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED` error code.
|
||||||
|
- [ ] `nsigner_client.c`: map error code 2002 to the new constant.
|
||||||
|
- [ ] `NSIGNER_INTEGRATION.md`: document the qrexec transport, nostr_index selector, and index_not_allowed error.
|
||||||
|
|
||||||
|
In `n_signer` (this repo):
|
||||||
|
|
||||||
|
- [ ] Update [`examples/get_pubkey_tcp.c`](../examples/get_pubkey_tcp.c) to optionally use the high-level `nostr_signer` API with `nostr_index` selector (demonstrates the new client API).
|
||||||
|
- [ ] Add a C example for the qrexec path using the new `nostr_signer_nsigner_qrexec` constructor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Open questions
|
||||||
|
|
||||||
|
1. **Should the qrexec transport live in `nostr_core_lib` or in a Qubes-specific addon?** The transport is only useful on Qubes OS, but `nostr_core_lib` is meant to be cross-platform. **Decision: put it in `nostr_core_lib` behind `NOSTR_ENABLE_NSIGNER_CLIENT` — it just won't work on non-Qubes hosts (execvp fails). No new feature flag needed.**
|
||||||
|
|
||||||
|
2. **Should `nostr_signer_nsigner_set_nostr_index` override `role` or coexist?** n_signer rejects ambiguous selectors (both `role` and `nostr_index` set = `ambiguous_role_selector`). **Decision: setting nostr_index clears role, and vice versa. The setter is exclusive.**
|
||||||
@@ -503,8 +503,8 @@ int socket_name_random(char *out, size_t out_len);
|
|||||||
/* Version information (auto-updated by build/version tooling) */
|
/* Version information (auto-updated by build/version tooling) */
|
||||||
#define NSIGNER_VERSION_MAJOR 0
|
#define NSIGNER_VERSION_MAJOR 0
|
||||||
#define NSIGNER_VERSION_MINOR 0
|
#define NSIGNER_VERSION_MINOR 0
|
||||||
#define NSIGNER_VERSION_PATCH 41
|
#define NSIGNER_VERSION_PATCH 42
|
||||||
#define NSIGNER_VERSION "v0.0.41"
|
#define NSIGNER_VERSION "v0.0.42"
|
||||||
|
|
||||||
|
|
||||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||||
|
|||||||
Reference in New Issue
Block a user