Compare commits

...
5 Commits
Author SHA1 Message Date
Laan Tungir 4b7c04dc49 v0.6.14 - Remove nostr_signer_nsigner_set_nostr_index compatibility shim; add nostr_signer_nsigner_set_derive_index 2026-08-06 13:07:11 -04:00
Laan Tungir c91670f7da Remove nostr_signer_nsigner_set_nostr_index compatibility shim
The shim expanded NIP-06 index N into role=main + role_path=m/44'/1237'/N'/0/0
client-side. It is removed — callers must now use
nostr_signer_nsigner_set_role_path explicitly.

Added nostr_signer_nsigner_set_derive_index for the derive verb's algorithm
index (previously set as a side effect of the shim).

Updated NSIGNER_INTEGRATION.md and plan docs to reflect the removal.

Tests: 8/8 pass.
2026-08-06 13:06:44 -04:00
Laan Tungir 71a72dc7ed Add nostr_signer_nsigner_from_client, nostr_signer_last_error, make from_transport public
- nostr_signer_nsigner_from_transport: made public (was static) so callers
  who open their own transport can create a signer from it.
- nostr_signer_nsigner_from_client: new constructor that creates a signer
  from an already-created low-level nsigner_client_t. Lets callers use both
  the high-level typed wrappers and the low-level nsigner_client_call for
  raw passthrough on the same connection.
- nostr_signer_last_error: returns the raw n_signer RPC error message
  (e.g. 'path_not_allowed', 'unknown_role') from the last failed call.
  Delegates to nsigner_client_last_error for the remote backend.
- Forward declarations for nsigner_transport_t and nsigner_client_t added
  to nostr_signer.h so the new constructors can be declared without
  pulling in the transport/client headers.
2026-08-06 10:17:17 -04:00
Laan Tungir d575e8cdbc Add 10 high-level algorithm-verb wrappers to nostr_signer_t
The high-level nostr_signer_t API only covered 6 of n_signer's 16 verbs
(the Nostr protocol verbs + derive_hmac). The other 10 (get_info,
get_public_key algorithm-based, sign, verify, encapsulate, decapsulate,
derive_shared_secret, otp encrypt/decrypt, mine_event) had no library
wrapper — clients had to drop down to the low-level nsigner_client_call
and hand-build cJSON params.

This adds typed wrappers for all 10:
- nostr_signer_get_info
- nostr_signer_get_public_key_alg
- nostr_signer_sign / nostr_signer_verify
- nostr_signer_encapsulate / nostr_signer_decapsulate
- nostr_signer_derive_shared_secret
- nostr_signer_otp_encrypt / nostr_signer_otp_decrypt
- nostr_signer_mine_event

The local backend returns NOSTR_ERROR_NOT_SUPPORTED for all 10 (they
require signer-side PQ crypto / OTP pads / key derivation the local
backend doesn't hold). The remote nsigner backend builds the correct
wire params and parses the result.

Also adds NOSTR_ERROR_NOT_SUPPORTED (-40) to nostr_common.h.

Tests: 2 new tests in nsigner_client_test.c — local-backend
NOT_SUPPORTED check for all 10 verbs, and a mock-transport test
verifying nostr_signer_sign emits the correct wire request. 8/8 pass.
2026-08-06 09:54:50 -04:00
Laan Tungir b235bd6585 Replace removed nostr_index selector with role+role_path
n_signer removed the bare nostr_index selector (error 2006) and index-on-
nostr-verbs (error 2007). The only accepted selector for nostr_* verbs is
now {"role","role_path"} sent together (2008/2009 if either is missing).

The remote signer backend was emitting {"nostr_index":N} or {"role"}
alone -- both now invalid. This commit:

- Replaces remote struct fields nostr_index/has_nostr_index with
  role_path[128]/has_role_path plus derive_index/has_derive_index
- Rewrites signer_remote_params_with_selector to emit both role and
  role_path in the options object
- Updates all three call sites (get_public_key, sign_event, encrypt_decrypt)
- Rewrites signer_remote_derive_hmac to emit {algorithm,index} using
  derive_index
- Adds nostr_signer_nsigner_set_role_path setter
- Repurposes nostr_signer_nsigner_set_nostr_index as a compatibility shim
  that expands N into role=main + role_path=m/44'/1237'/N'/0/0
- Updates nsigner_client_test to send and assert the role+role_path selector
- Updates NSIGNER_INTEGRATION.md (new section 2.1.1) and plan docs

Builds clean (38/39 tests; nip34_test failure is pre-existing/unrelated).
nsigner_client_test 6/6 pass, signer_modules_test 26/26 pass.
2026-08-06 09:42:40 -04:00
10 changed files with 1225 additions and 61 deletions
+1 -1
View File
@@ -1 +1 @@
0.6.13
0.6.14
+91 -1
View File
@@ -39,6 +39,64 @@ int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
char** plaintext_out);
```
### 1.1 Algorithm-based verbs (remote nsigner backend only)
These wrap n_signer's algorithm-based wire verbs. The local backend returns
`NOSTR_ERROR_NOT_SUPPORTED` for all of them — they require signer-side key
derivation / PQ crypto / OTP pads that the local backend does not hold.
```c
int nostr_signer_get_info(nostr_signer_t* signer, cJSON** info_out);
int nostr_signer_get_public_key_alg(nostr_signer_t* signer,
const char* algorithm, int index,
char** result_json_out);
int nostr_signer_sign(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme, /* "schnorr"|"ecdsa"|NULL */
const unsigned char* msg, size_t msg_len,
char** result_json_out);
int nostr_signer_verify(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
const unsigned char* sig, size_t sig_len,
int* valid_out);
int nostr_signer_encapsulate(nostr_signer_t* signer,
const char* peer_pubkey_hex,
char** result_json_out);
int nostr_signer_decapsulate(nostr_signer_t* signer, int index,
const char* ciphertext_hex,
char** result_json_out);
int nostr_signer_derive_shared_secret(nostr_signer_t* signer, int index,
const char* peer_pubkey_hex,
char** result_json_out);
int nostr_signer_otp_encrypt(nostr_signer_t* signer,
const char* plaintext_b64,
const char* encoding, /* "ascii"|"binary"|NULL */
char** result_json_out);
int nostr_signer_otp_decrypt(nostr_signer_t* signer,
const char* ciphertext,
const char* encoding,
char** result_json_out);
int nostr_signer_mine_event(nostr_signer_t* signer,
const cJSON* unsigned_event,
int difficulty, int timeout_sec, int threads,
cJSON** signed_event_out);
```
The `*_result_json_out` functions return a malloc'd copy of the raw JSON
result string (caller frees). `nostr_signer_get_info` and
`nostr_signer_mine_event` return parsed cJSON objects (caller frees).
## 2) Remote signer factories + transport API (exact signatures)
### 2.1 Signer-side remote factories (`nostr_core/nostr_signer.h`)
@@ -49,13 +107,45 @@ 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 full BIP-44 role_path. n_signer requires BOTH role and role_path
* together for nostr_* verbs; set the role via the constructor and the path
* here. */
int nostr_signer_nsigner_set_role_path(nostr_signer_t* signer, const char* role_path);
/* Set the algorithm derivation index used by the derive verb
* (nostr_signer_derive_hmac). The derive verb is algorithm-based and
* requires an explicit index (no default). Pass a negative value to clear. */
int nostr_signer_nsigner_set_derive_index(nostr_signer_t* signer, int index);
#endif
```
### 2.1.1 Key selector model
n_signer's `nostr_*` verbs select a key via a **combined** `role` + `role_path`
selector — both fields are required together. The remote backend emits:
```json
{"role":"<name>","role_path":"m/44'/1237'/N'/0/0"}
```
as the trailing options object of every `nostr_*` request. Omitting either
field is rejected by n_signer with `2008 role_required` / `2009 path_required`.
The bare `{"nostr_index":N}` selector is **removed** on the n_signer side
(error `2006 nostr_index_deprecated`); this library never emits it.
Set the selector with **`nostr_signer_nsigner_set_role_path(signer, path)`** —
pass the role name to the constructor and the full path here. This is the only
way to select a key for `nostr_*` verbs.
For the algorithm-based `derive` verb (used by `nostr_signer_derive_hmac`), the
backend emits `{"algorithm":"secp256k1","index":N}`. The index comes from the
last `nostr_signer_nsigner_set_derive_index` call; if unset, the request is
sent without `index` and n_signer rejects it with `missing_index`.
### 2.2 Transport constructors + framed I/O + discovery (`nostr_core/nsigner_transport.h`)
```c
@@ -132,7 +222,7 @@ The remote signer backend uses n_signer's role-based Nostr methods:
These wire names are distinct from both the public `nostr_signer_*` C API and the
unprefixed NIP-46 method names. In particular, n_signer's unprefixed `get_public_key`
is algorithm-based; this library uses `nostr_get_public_key` because remote signer
factories select keys by `role` or `nostr_index`.
factories select keys by `role` + `role_path` (see §2.1.1).
### Auth and framing notes
+1
View File
@@ -28,6 +28,7 @@
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
#define NOSTR_ERROR_NOT_SUPPORTED -40 /* verb not supported by this backend */
/* nsigner remote backend errors */
#define NOSTR_ERROR_NSIGNER_POLICY_DENIED -2001
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
+2 -2
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.6.13"
#define VERSION "v0.6.14"
#define VERSION_MAJOR 0
#define VERSION_MINOR 6
#define VERSION_PATCH 13
#define VERSION_PATCH 14
/*
* NOSTR Core Library - Complete API Reference
+766 -50
View File
@@ -1,5 +1,6 @@
#include "nostr_signer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -30,8 +31,10 @@ 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 */
char role_path[128]; /* full BIP-44 path; required with role for nostr_* verbs */
int has_role_path; /* 1 if role_path was set explicitly */
int derive_index; /* algorithm index for the derive verb; -1 = not set */
int has_derive_index; /* 1 if derive_index was set */
} remote;
#endif
} u;
@@ -306,7 +309,10 @@ static int signer_local_derive_hmac(nostr_signer_t* signer,
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role, int has_nostr_index, int nostr_index) {
static cJSON* signer_remote_params_with_selector(cJSON* params,
const char* role,
const char* role_path,
int has_role_path) {
cJSON* selector;
if (params == NULL) {
@@ -316,19 +322,9 @@ 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') {
/* n_signer requires BOTH role and role_path together for nostr_* verbs.
* If neither is set, emit nothing (caller handles the error). */
if ((role == NULL || role[0] == '\0') && !has_role_path) {
return params;
}
@@ -338,7 +334,12 @@ static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role
return NULL;
}
cJSON_AddStringToObject(selector, "role", role);
if (role != NULL && role[0] != '\0') {
cJSON_AddStringToObject(selector, "role", role);
}
if (has_role_path && role_path != NULL && role_path[0] != '\0') {
cJSON_AddStringToObject(selector, "role_path", role_path);
}
cJSON_AddItemToArray(params, selector);
return params;
}
@@ -354,8 +355,8 @@ static int signer_remote_get_public_key(nostr_signer_t* signer, char out_pubkey_
}
params = signer_remote_params_with_selector(NULL, signer->u.remote.role,
signer->u.remote.has_nostr_index,
signer->u.remote.nostr_index);
signer->u.remote.role_path,
signer->u.remote.has_role_path);
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
@@ -406,8 +407,8 @@ static int signer_remote_sign_event(nostr_signer_t* signer, const cJSON* unsigne
free(event_json);
params = signer_remote_params_with_selector(params, signer->u.remote.role,
signer->u.remote.has_nostr_index,
signer->u.remote.nostr_index);
signer->u.remote.role_path,
signer->u.remote.has_role_path);
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
@@ -466,8 +467,8 @@ static int signer_remote_encrypt_decrypt(nostr_signer_t* signer,
cJSON_AddItemToArray(params, cJSON_CreateString(in));
params = signer_remote_params_with_selector(params, signer->u.remote.role,
signer->u.remote.has_nostr_index,
signer->u.remote.nostr_index);
signer->u.remote.role_path,
signer->u.remote.has_role_path);
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
@@ -546,22 +547,18 @@ static int signer_remote_derive_hmac(nostr_signer_t* signer,
}
cJSON_AddItemToArray(params, cJSON_CreateString(data));
/* Build the options object with algorithm:"secp256k1" and the selector
* (role or nostr_index). The derive verb requires index, so when
* has_nostr_index is set we include it; otherwise we include role (the
* nsigner derive handler requires index, so callers using the remote
* backend must set nostr_index via nostr_signer_nsigner_set_nostr_index
* before calling derive_hmac). */
/* Build the options object with algorithm:"secp256k1" and the index.
* The derive verb is algorithm-based and requires an explicit index
* (no default). The index is set via nostr_signer_nsigner_set_derive_index.
* Without an index the signer rejects the request with missing_index. */
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", "secp256k1");
if (signer->u.remote.has_nostr_index) {
cJSON_AddNumberToObject(opts, "index", signer->u.remote.nostr_index);
} else if (signer->u.remote.role[0] != '\0') {
cJSON_AddStringToObject(opts, "role", signer->u.remote.role);
if (signer->u.remote.has_derive_index) {
cJSON_AddNumberToObject(opts, "index", signer->u.remote.derive_index);
}
cJSON_AddItemToArray(params, opts);
@@ -593,6 +590,483 @@ static int signer_remote_derive_hmac(nostr_signer_t* signer,
return NOSTR_SUCCESS;
}
/* ---- Remote implementations of the algorithm-based verbs ---- */
static int signer_remote_get_info(nostr_signer_t* signer, cJSON** info_out) {
cJSON* params;
cJSON* result = NULL;
int rc;
if (!signer || !info_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*info_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
rc = nsigner_client_call(signer->u.remote.client, "get_info", params, &result);
if (rc != NOSTR_SUCCESS) {
return rc;
}
if (cJSON_IsString(result) && result->valuestring != NULL) {
cJSON* parsed = cJSON_Parse(result->valuestring);
cJSON_Delete(result);
if (parsed == NULL || !cJSON_IsObject(parsed)) {
cJSON_Delete(parsed);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
*info_out = parsed;
return NOSTR_SUCCESS;
}
if (cJSON_IsObject(result)) {
*info_out = cJSON_Duplicate(result, 1);
cJSON_Delete(result);
if (*info_out == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
return NOSTR_SUCCESS;
}
cJSON_Delete(result);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
/* Helper: call a verb whose result is a JSON-object string, return it as a
* malloc'd copy of the raw result string (caller frees). Used for verbs
* where the caller wants the full JSON object string. */
static int signer_remote_call_result_string(nostr_signer_t* signer,
const char* method,
cJSON* params,
char** out) {
cJSON* result = NULL;
const char* s;
size_t n;
int rc;
if (!signer || !method || !out) {
if (params) cJSON_Delete(params);
return NOSTR_ERROR_INVALID_INPUT;
}
*out = NULL;
rc = nsigner_client_call(signer->u.remote.client, method, params, &result);
if (rc != NOSTR_SUCCESS) {
return rc;
}
if (!cJSON_IsString(result) || result->valuestring == NULL) {
cJSON_Delete(result);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
s = result->valuestring;
n = strlen(s);
*out = (char*)malloc(n + 1);
if (*out == NULL) {
cJSON_Delete(result);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(*out, s, n + 1);
cJSON_Delete(result);
return NOSTR_SUCCESS;
}
static int signer_remote_get_public_key_alg(nostr_signer_t* signer,
const char* algorithm, int index,
char** result_json_out) {
cJSON* params;
cJSON* opts;
if (!signer || !algorithm || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", algorithm);
cJSON_AddNumberToObject(opts, "index", index);
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "get_public_key", params, result_json_out);
}
static int signer_remote_sign(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
char** result_json_out) {
cJSON* params;
cJSON* opts;
char* msg_hex;
if (!signer || !algorithm || !msg || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
msg_hex = (char*)malloc(msg_len * 2 + 1);
if (msg_hex == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
nostr_bytes_to_hex(msg, msg_len, msg_hex);
params = cJSON_CreateArray();
if (params == NULL) {
free(msg_hex);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(msg_hex));
free(msg_hex);
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", algorithm);
cJSON_AddNumberToObject(opts, "index", index);
if (scheme != NULL && scheme[0] != '\0') {
cJSON_AddStringToObject(opts, "scheme", scheme);
}
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "sign", params, result_json_out);
}
static int signer_remote_verify(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
const unsigned char* sig, size_t sig_len,
int* valid_out) {
cJSON* params;
cJSON* opts;
cJSON* result = NULL;
cJSON* parsed = NULL;
cJSON* valid_item = NULL;
char* msg_hex;
char* sig_hex;
int rc;
if (!signer || !algorithm || !msg || !sig || !valid_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*valid_out = 0;
msg_hex = (char*)malloc(msg_len * 2 + 1);
if (msg_hex == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
nostr_bytes_to_hex(msg, msg_len, msg_hex);
sig_hex = (char*)malloc(sig_len * 2 + 1);
if (sig_hex == NULL) {
free(msg_hex);
return NOSTR_ERROR_MEMORY_FAILED;
}
nostr_bytes_to_hex(sig, sig_len, sig_hex);
params = cJSON_CreateArray();
if (params == NULL) {
free(msg_hex);
free(sig_hex);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(msg_hex));
cJSON_AddItemToArray(params, cJSON_CreateString(sig_hex));
free(msg_hex);
free(sig_hex);
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", algorithm);
cJSON_AddNumberToObject(opts, "index", index);
if (scheme != NULL && scheme[0] != '\0') {
cJSON_AddStringToObject(opts, "scheme", scheme);
}
cJSON_AddItemToArray(params, opts);
rc = nsigner_client_call(signer->u.remote.client, "verify", params, &result);
if (rc != NOSTR_SUCCESS) {
return rc;
}
/* result is a JSON string: {"valid":true,"algorithm":"..."} */
if (!cJSON_IsString(result) || result->valuestring == NULL) {
cJSON_Delete(result);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
parsed = cJSON_Parse(result->valuestring);
cJSON_Delete(result);
if (parsed == NULL) {
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
valid_item = cJSON_GetObjectItemCaseSensitive(parsed, "valid");
if (valid_item == NULL || !cJSON_IsBool(valid_item)) {
cJSON_Delete(parsed);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
*valid_out = cJSON_IsTrue(valid_item) ? 1 : 0;
cJSON_Delete(parsed);
return NOSTR_SUCCESS;
}
static int signer_remote_encapsulate(nostr_signer_t* signer,
const char* peer_pubkey_hex,
char** result_json_out) {
cJSON* params;
cJSON* opts;
if (!signer || !peer_pubkey_hex || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey_hex));
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "encapsulate", params, result_json_out);
}
static int signer_remote_decapsulate(nostr_signer_t* signer, int index,
const char* ciphertext_hex,
char** result_json_out) {
cJSON* params;
cJSON* opts;
if (!signer || !ciphertext_hex || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext_hex));
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", "ml-kem-768");
cJSON_AddNumberToObject(opts, "index", index);
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "decapsulate", params, result_json_out);
}
static int signer_remote_derive_shared_secret(nostr_signer_t* signer, int index,
const char* peer_pubkey_hex,
char** result_json_out) {
cJSON* params;
cJSON* opts;
if (!signer || !peer_pubkey_hex || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey_hex));
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", "x25519");
cJSON_AddNumberToObject(opts, "index", index);
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "derive_shared_secret", params, result_json_out);
}
static int signer_remote_otp_encrypt(nostr_signer_t* signer,
const char* plaintext_b64,
const char* encoding,
char** result_json_out) {
cJSON* params;
cJSON* opts;
if (!signer || !plaintext_b64 || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(plaintext_b64));
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", "otp");
if (encoding != NULL && encoding[0] != '\0') {
cJSON_AddStringToObject(opts, "encoding", encoding);
}
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "encrypt", params, result_json_out);
}
static int signer_remote_otp_decrypt(nostr_signer_t* signer,
const char* ciphertext,
const char* encoding,
char** result_json_out) {
cJSON* params;
cJSON* opts;
if (!signer || !ciphertext || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*result_json_out = NULL;
params = cJSON_CreateArray();
if (params == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(ciphertext));
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(opts, "algorithm", "otp");
if (encoding != NULL && encoding[0] != '\0') {
cJSON_AddStringToObject(opts, "encoding", encoding);
}
cJSON_AddItemToArray(params, opts);
return signer_remote_call_result_string(signer, "decrypt", params, result_json_out);
}
static int signer_remote_mine_event(nostr_signer_t* signer,
const cJSON* unsigned_event,
int difficulty, int timeout_sec, int threads,
cJSON** signed_event_out) {
cJSON* params;
cJSON* opts;
cJSON* result = NULL;
cJSON* parsed = NULL;
char* event_json;
int rc;
if (!signer || !unsigned_event || !signed_event_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*signed_event_out = NULL;
event_json = cJSON_PrintUnformatted((cJSON*)unsigned_event);
if (event_json == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
params = cJSON_CreateArray();
if (params == NULL) {
free(event_json);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
free(event_json);
opts = cJSON_CreateObject();
if (opts == NULL) {
cJSON_Delete(params);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (signer->u.remote.role[0] != '\0') {
cJSON_AddStringToObject(opts, "role", signer->u.remote.role);
}
if (signer->u.remote.has_role_path && signer->u.remote.role_path[0] != '\0') {
cJSON_AddStringToObject(opts, "role_path", signer->u.remote.role_path);
}
if (difficulty > 0) {
cJSON_AddNumberToObject(opts, "difficulty", difficulty);
}
if (timeout_sec > 0) {
cJSON_AddNumberToObject(opts, "timeout_sec", timeout_sec);
}
if (threads > 0) {
cJSON_AddNumberToObject(opts, "threads", threads);
}
cJSON_AddItemToArray(params, opts);
rc = nsigner_client_call(signer->u.remote.client, "nostr_mine_event", params, &result);
if (rc != NOSTR_SUCCESS) {
return rc;
}
if (cJSON_IsString(result) && result->valuestring != NULL) {
parsed = cJSON_Parse(result->valuestring);
cJSON_Delete(result);
if (parsed == NULL || !cJSON_IsObject(parsed)) {
cJSON_Delete(parsed);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
*signed_event_out = parsed;
return NOSTR_SUCCESS;
}
if (cJSON_IsObject(result)) {
*signed_event_out = cJSON_Duplicate(result, 1);
cJSON_Delete(result);
if (*signed_event_out == NULL) {
return NOSTR_ERROR_MEMORY_FAILED;
}
return NOSTR_SUCCESS;
}
cJSON_Delete(result);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]) {
@@ -627,6 +1101,7 @@ void nostr_signer_free(nostr_signer_t* signer) {
signer->u.remote.client = NULL;
}
memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role));
memset(signer->u.remote.role_path, 0, sizeof(signer->u.remote.role_path));
}
#endif
@@ -766,23 +1241,214 @@ int nostr_signer_derive_hmac(nostr_signer_t* signer,
return NOSTR_ERROR_INVALID_INPUT;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
static nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role) {
nsigner_client_t* client = NULL;
nostr_signer_t* signer = NULL;
/* ---- Public dispatch for the algorithm-based verbs ---- */
if (transport == NULL) {
return NULL;
int nostr_signer_get_info(nostr_signer_t* signer, cJSON** info_out) {
if (!signer || !info_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
client = nsigner_client_new(transport);
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_get_info(signer, info_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_get_public_key_alg(nostr_signer_t* signer,
const char* algorithm, int index,
char** result_json_out) {
if (!signer || !algorithm || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_get_public_key_alg(signer, algorithm, index, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_sign(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
char** result_json_out) {
if (!signer || !algorithm || !msg || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_sign(signer, algorithm, index, scheme, msg, msg_len, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_verify(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
const unsigned char* sig, size_t sig_len,
int* valid_out) {
if (!signer || !algorithm || !msg || !sig || !valid_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_verify(signer, algorithm, index, scheme, msg, msg_len, sig, sig_len, valid_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_encapsulate(nostr_signer_t* signer,
const char* peer_pubkey_hex,
char** result_json_out) {
if (!signer || !peer_pubkey_hex || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_encapsulate(signer, peer_pubkey_hex, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_decapsulate(nostr_signer_t* signer, int index,
const char* ciphertext_hex,
char** result_json_out) {
if (!signer || !ciphertext_hex || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_decapsulate(signer, index, ciphertext_hex, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_derive_shared_secret(nostr_signer_t* signer, int index,
const char* peer_pubkey_hex,
char** result_json_out) {
if (!signer || !peer_pubkey_hex || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_derive_shared_secret(signer, index, peer_pubkey_hex, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_otp_encrypt(nostr_signer_t* signer,
const char* plaintext_b64,
const char* encoding,
char** result_json_out) {
if (!signer || !plaintext_b64 || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_otp_encrypt(signer, plaintext_b64, encoding, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_otp_decrypt(nostr_signer_t* signer,
const char* ciphertext,
const char* encoding,
char** result_json_out) {
if (!signer || !ciphertext || !result_json_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_otp_decrypt(signer, ciphertext, encoding, result_json_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
int nostr_signer_mine_event(nostr_signer_t* signer,
const cJSON* unsigned_event,
int difficulty, int timeout_sec, int threads,
cJSON** signed_event_out) {
if (!signer || !unsigned_event || !signed_event_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
return NOSTR_ERROR_NOT_SUPPORTED;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
return signer_remote_mine_event(signer, unsigned_event, difficulty, timeout_sec, threads, signed_event_out);
}
#endif
return NOSTR_ERROR_INVALID_INPUT;
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
nostr_signer_t* nostr_signer_nsigner_from_client(nsigner_client_t* client, const char* role) {
nostr_signer_t* signer = NULL;
if (client == NULL) {
return NULL;
}
signer = (nostr_signer_t*)calloc(1, sizeof(*signer));
if (signer == NULL) {
nsigner_client_free(client);
return NULL;
}
@@ -796,6 +1462,21 @@ static nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t*
return signer;
}
nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role) {
nsigner_client_t* client = NULL;
if (transport == NULL) {
return NULL;
}
client = nsigner_client_new(transport);
if (client == NULL) {
return NULL;
}
return nostr_signer_nsigner_from_client(client, role);
}
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms) {
nsigner_transport_t* transport = NULL;
char discovered[8][64];
@@ -848,6 +1529,18 @@ int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
return nsigner_client_set_auth(signer->u.remote.client, auth_privkey, label);
}
const char* nostr_signer_last_error(const nostr_signer_t* signer) {
if (signer == NULL) {
return "null signer";
}
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE && signer->u.remote.client != NULL) {
return nsigner_client_last_error(signer->u.remote.client);
}
#endif
return "no error details available";
}
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;
@@ -855,7 +1548,7 @@ nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char*
return nostr_signer_nsigner_from_transport(transport, role);
}
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index) {
int nostr_signer_nsigner_set_role_path(nostr_signer_t* signer, const char* role_path) {
if (signer == NULL) {
return NOSTR_ERROR_INVALID_INPUT;
}
@@ -864,15 +1557,38 @@ int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index
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;
if (role_path == NULL || role_path[0] == '\0') {
/* Clear the role_path selector */
signer->u.remote.has_role_path = 0;
signer->u.remote.role_path[0] = '\0';
} 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));
size_t n = strlen(role_path);
if (n >= sizeof(signer->u.remote.role_path)) {
return NOSTR_ERROR_INVALID_INPUT;
}
memcpy(signer->u.remote.role_path, role_path, n);
signer->u.remote.role_path[n] = '\0';
signer->u.remote.has_role_path = 1;
}
return NOSTR_SUCCESS;
}
int nostr_signer_nsigner_set_derive_index(nostr_signer_t* signer, int 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 (index < 0) {
signer->u.remote.has_derive_index = 0;
signer->u.remote.derive_index = -1;
} else {
signer->u.remote.derive_index = index;
signer->u.remote.has_derive_index = 1;
}
return NOSTR_SUCCESS;
+119 -4
View File
@@ -15,6 +15,12 @@ typedef struct nostr_signer nostr_signer_t;
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
void nostr_signer_free(nostr_signer_t* signer);
/* Returns a human-readable error string from the last failed call. For the
* remote nsigner backend this is the raw n_signer RPC error message (e.g.
* "path_not_allowed", "unknown_role"). For the local backend returns a
* generic string. Safe to call after any wrapper returns non-NOSTR_SUCCESS. */
const char* nostr_signer_last_error(const nostr_signer_t* signer);
/* Core verbs */
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
@@ -47,7 +53,109 @@ int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
const char* ciphertext,
char** plaintext_out);
/* ---- Algorithm-based verbs (remote nsigner backend only) ----
*
* These wrap n_signer's algorithm-based wire verbs. The local backend
* returns NOSTR_ERROR_NOT_SUPPORTED for all of them — they require
* signer-side key derivation / PQ crypto / OTP pads that the local
* backend does not hold. Use a remote nsigner signer for these.
*
* `algorithm` is one of: "secp256k1", "ed25519", "x25519", "ml-dsa-65",
* "slh-dsa-128s", "ml-kem-768", "otp" (see n_signer README §4.4).
* `index` is the algorithm derivation index substituted into the alg's
* derivation path. `scheme` is secp256k1-only: "schnorr" (default) or
* "ecdsa"; pass NULL for the default.
*/
/* Signer metadata. Returns a cJSON object (caller frees) with name,
* implementation, version, verbs, algorithms, etc. */
int nostr_signer_get_info(nostr_signer_t* signer, cJSON** info_out);
/* Algorithm-based public key. Returns a malloc'd string (caller frees):
* for most algorithms this is a JSON object string
* {"algorithm":"...","public_key":"<hex>","key_id":"<16hex>"}; for
* nostr_get_public_key-style plain output see nostr_signer_get_public_key. */
int nostr_signer_get_public_key_alg(nostr_signer_t* signer,
const char* algorithm, int index,
char** result_json_out);
/* Sign a message. msg/msg_len are raw bytes; the wire sends hex.
* Returns a malloc'd JSON string (caller frees):
* {"signature":"<hex>","algorithm":"...","key_id":"<16hex>"}. */
int nostr_signer_sign(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
char** result_json_out);
/* Verify a signature. sig/sig_len are raw bytes. Sets *valid_out to 1
* or 0. Returns NOSTR_SUCCESS or an error code. */
int nostr_signer_verify(nostr_signer_t* signer,
const char* algorithm, int index,
const char* scheme,
const unsigned char* msg, size_t msg_len,
const unsigned char* sig, size_t sig_len,
int* valid_out);
/* ML-KEM-768 encapsulate. peer_pubkey_hex is the recipient's 1184-byte
* (2368 hex) ML-KEM public key. Returns malloc'd JSON strings (caller
* frees): {"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"}. */
int nostr_signer_encapsulate(nostr_signer_t* signer,
const char* peer_pubkey_hex,
char** result_json_out);
/* ML-KEM-768 decapsulate. ciphertext_hex is the encapsulated ciphertext.
* Returns malloc'd JSON string (caller frees):
* {"shared_secret":"<hex>","algorithm":"ml-kem-768"}. */
int nostr_signer_decapsulate(nostr_signer_t* signer, int index,
const char* ciphertext_hex,
char** result_json_out);
/* X25519 derive_shared_secret. peer_pubkey_hex is the peer's 32-byte
* (64 hex) X25519 public key. Returns malloc'd JSON string (caller frees):
* {"shared_secret":"<hex>","algorithm":"x25519"}. */
int nostr_signer_derive_shared_secret(nostr_signer_t* signer, int index,
const char* peer_pubkey_hex,
char** result_json_out);
/* OTP encrypt. plaintext_b64 is base64-encoded plaintext. encoding is
* "ascii" (default) or "binary"; pass NULL for default. Returns malloc'd
* JSON string (caller frees) with ciphertext, encoding, pad offsets. */
int nostr_signer_otp_encrypt(nostr_signer_t* signer,
const char* plaintext_b64,
const char* encoding,
char** result_json_out);
/* OTP decrypt. ciphertext is the ASCII-armored or base64 blob. encoding
* is "ascii"/"binary" or NULL for auto-detect. Returns malloc'd JSON
* string (caller frees) with plaintext + pad offsets. */
int nostr_signer_otp_decrypt(nostr_signer_t* signer,
const char* ciphertext,
const char* encoding,
char** result_json_out);
/* Nostr mine-event (proof-of-work). difficulty is target leading zero
* bits; timeout_sec and threads are optional (pass 0/1 for defaults).
* Returns the signed+mined event as a cJSON object (caller frees). */
int nostr_signer_mine_event(nostr_signer_t* signer,
const cJSON* unsigned_event,
int difficulty, int timeout_sec, int threads,
cJSON** signed_event_out);
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
/* Forward declaration so from_transport can be declared without including
* nsigner_transport.h (which would create a header dependency cycle). */
typedef struct nsigner_transport nsigner_transport_t;
typedef struct nsigner_client nsigner_client_t;
/* Create a remote nsigner signer from an already-opened transport. Takes
* ownership of the transport. This is the most flexible constructor — the
* caller opens the transport (unix/tcp/serial/fds/qrexec) and passes it in. */
nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role);
/* Create a remote nsigner signer from an already-created low-level client.
* Takes ownership of the client (freed by nostr_signer_free). Useful when
* the caller needs both the high-level typed wrappers and the low-level
* nsigner_client_call for raw passthrough on the same connection. */
nostr_signer_t* nostr_signer_nsigner_from_client(nsigner_client_t* client, const char* role);
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
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);
@@ -58,12 +166,19 @@ 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).
* Set the role_path selector for the remote nsigner backend. n_signer now
* requires BOTH "role" and "role_path" together for nostr_* verbs; set the
* role via the constructor (or keep the constructor default) and set the full
* BIP-44 derivation path here. Returns NOSTR_SUCCESS or an error code.
*/
int nostr_signer_nsigner_set_role_path(nostr_signer_t* signer, const char* role_path);
/*
* Set the algorithm derivation index used by the derive verb
* (nostr_signer_derive_hmac). The derive verb is algorithm-based and
* requires an explicit index (no default). Pass a negative value to clear.
* Returns NOSTR_SUCCESS or an error code.
*/
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
int nostr_signer_nsigner_set_derive_index(nostr_signer_t* signer, int index);
#endif
#ifdef __cplusplus
+75
View File
@@ -0,0 +1,75 @@
# Plan: n_signer Selector Rewrite for nostr_core_lib — COMPLETED
> **Status: Done.** The remote signer backend now emits the combined
> `role` + `role_path` selector required by current n_signer. The bare
> `nostr_index` selector is no longer emitted. `make` builds clean and the
> nsigner client test passes.
## Context
n_signer removed the `nostr_index` selector and `index`-on-nostr-verbs
([`n_signer/plans/role_path_authorization.md`](../../n_signer/plans/role_path_authorization.md)).
The only accepted selector for `nostr_*` verbs is now
`{"role":"<name>","role_path":"<full-path>"}` sent together. Either field
alone is rejected (`2008 role_required` / `2009 path_required`); a bare
`nostr_index` is rejected with `2006 nostr_index_deprecated`.
`nostr_core_lib`'s remote signer backend was emitting `{"nostr_index":N}` or
`{"role":"..."}` alone — both now invalid.
## Changes made
### `nostr_core/nostr_signer.h`
- Documented the new `nostr_signer_nsigner_set_role_path` setter.
- Added `nostr_signer_nsigner_set_derive_index` for the derive verb's
algorithm index.
- **Removed** `nostr_signer_nsigner_set_nostr_index` (the compatibility shim).
Callers must now use `set_role_path` explicitly.
### `nostr_core/nostr_signer.c`
- Replaced the `remote` struct fields `nostr_index`/`has_nostr_index` with
`role_path[128]`/`has_role_path` plus `derive_index`/`has_derive_index`
(the latter feeds the algorithm-based `derive` verb).
- Rewrote `signer_remote_params_with_selector` to emit both `role` and
`role_path` in the options object.
- Updated all three call sites (`get_public_key`, `sign_event`,
`encrypt_decrypt`) to the new signature.
- Rewrote `signer_remote_derive_hmac` to emit `{"algorithm":"secp256k1",
"index":N}` using `derive_index` (set by `set_derive_index`).
- Added `nostr_signer_nsigner_set_role_path`.
- Added `nostr_signer_nsigner_set_derive_index`.
- **Removed** `nostr_signer_nsigner_set_nostr_index` (the compatibility shim).
- Added `<stdio.h>` for `snprintf`.
- `nostr_signer_free` now also zeroes `role_path`.
### `tests/nsigner_client_test.c`
- The mock-server fds test now sends a `{"role":"main",
"role_path":"m/44'/1237'/0'/0/0"}` selector and the child asserts both
fields are present in the framed request.
### Docs
- `NSIGNER_INTEGRATION.md` §2.1 and new §2.1.1 document the selector model.
- `plans/n_signer_verb_migration.md` and `plans/nsigner_integration_plan.md`
updated with notes pointing here.
## Verification
- `make` builds clean.
- `nsigner_client_test` passes (mock transport round-trip with the new
selector).
- `grep -rn 'nostr_index' nostr_core/nostr_signer.c` returns no matches
(the shim is removed).
## Downstream consumers still needing updates
These repos link `nostr_core_lib` and call `nostr_signer_nsigner_*`. The
`set_nostr_index` shim is **removed** — they must be updated to use
`set_role_path` explicitly:
- `sovereign_browser` — `src/login_dialog.c`, `src/agent_login.c`,
`src/key_store.c` (uses `set_nostr_index`; must switch to `set_role_path`).
- `nostr_terminal` — has its own hand-rolled `nsigner_client.c` that still
emits `{"nostr_index":N}` directly (NOT via this lib); must be rewritten
independently.
- `laantungir_website` — raw JS JSON-RPC, emits `{"nostr_index":N}`; must be
rewritten independently.
+8 -2
View File
@@ -20,7 +20,13 @@ The Nostr protocol verbs gained a `nostr_` prefix. The algorithm-based verbs (`s
| `nip44_decrypt` | `nostr_nip44_decrypt` |
| `get_public_key` (role-based) | `nostr_get_public_key` |
**Important distinction:** `get_public_key` is now **algorithm-based** (takes `algorithm`+`index`). The role-based Nostr pubkey verb is `nostr_get_public_key` (takes `nostr_index`/`role`). Since `nostr_core_lib`'s remote signer always uses role-based selectors (`nostr_index`/`role`), the correct replacement is `nostr_get_public_key`.
**Important distinction:** `get_public_key` is now **algorithm-based** (takes `algorithm`+`index`). The role-based Nostr pubkey verb is `nostr_get_public_key` (takes `role`+`role_path`). Since `nostr_core_lib`'s remote signer always uses role-based selectors, the correct replacement is `nostr_get_public_key`.
> **Selector update (post-migration):** n_signer subsequently removed the
> `nostr_index` and `index` selectors for `nostr_*` verbs entirely. The only
> accepted selector is now `{"role":"<name>","role_path":"<full-path>"}` sent
> together. `nostr_core_lib` was updated to emit both fields; see
> `NSIGNER_INTEGRATION.md` §2.1.1 and `plans/n_signer_selector_rewrite.md`.
## What does NOT change
@@ -43,7 +49,7 @@ This is the only file that calls n_signer verbs via `nsigner_client_call()`. All
| 498 | `"nip44_encrypt"` | `"nostr_nip44_encrypt"` |
| 505 | `"nip44_decrypt"` | `"nostr_nip44_decrypt"` |
These are simple string-literal replacements. The `signer_remote_params_with_selector()` function (line 289) builds `{"nostr_index": N}` or `{"role": "..."}` selectors — these are still valid for the `nostr_*` verbs, so no selector changes needed.
These are simple string-literal replacements. The `signer_remote_params_with_selector()` function (line 289) builds `{"nostr_index": N}` or `{"role": "..."}` selectors — **these were subsequently invalidated** by n_signer's selector rewrite (the `nostr_index` selector is removed and `role` alone is rejected). That rewrite was handled in a follow-up; see `plans/n_signer_selector_rewrite.md`.
**Response format note:** `nostr_get_public_key` returns a plain 64-hex-char string by default (same as the old role-based `get_public_key`). The existing parsing at line 348 (`strlen(result->valuestring) != 64`) continues to work. If the caller ever passes `{"format":"structured"}`, the response would be a JSON object string — but the current code doesn't request structured format, so no change needed.
+8 -1
View File
@@ -9,9 +9,16 @@
> `nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_encrypt`,
> `nostr_nip04_decrypt`, `nostr_nip44_encrypt`, and `nostr_nip44_decrypt`.
> The unprefixed `get_public_key` method is algorithm-based and is not used by the
> role/`nostr_index` selectors in this library. Public `nostr_signer_*` C API names
> role/`role_path` selectors in this library. Public `nostr_signer_*` C API names
> and standard NIP-46 method names are unchanged.
>
> **Selector update:** n_signer removed the bare `nostr_index` selector and
> `index`-on-nostr-verbs. The only accepted selector for `nostr_*` verbs is now
> `{"role":"<name>","role_path":"<full-path>"}` sent together. This library
> emits both fields. The `nostr_signer_nsigner_set_nostr_index` compatibility
> shim was removed; callers must use `nostr_signer_nsigner_set_role_path`
> explicitly. See `NSIGNER_INTEGRATION.md` §2.1.1.
>
> This plan originally described pulling the **caller-side** signer-integration glue out of per-project implementations and into `nostr_core_lib`,
> so any project that already links the library can sign **locally**, via a **running
> n_signer process**, or via a **USB hardware signer** — with the same code.
+154
View File
@@ -255,6 +255,12 @@ static int test_fds_transport_round_trip_via_client(void) {
free(req);
_exit(6);
}
/* The new n_signer wire contract requires both role and role_path. */
if (strstr(req, "\"role\":\"main\"") == NULL ||
strstr(req, "\"role_path\":\"m/44'/1237'/0'/0/0\"") == NULL) {
free(req);
_exit(9);
}
free(req);
out_hdr[0] = (unsigned char)((res_len >> 24) & 0xFFU);
@@ -293,6 +299,15 @@ static int test_fds_transport_round_trip_via_client(void) {
if (params == NULL) {
goto cleanup;
}
{
cJSON* selector = cJSON_CreateObject();
if (selector == NULL) {
goto cleanup;
}
cJSON_AddStringToObject(selector, "role", "main");
cJSON_AddStringToObject(selector, "role_path", "m/44'/1237'/0'/0/0");
cJSON_AddItemToArray(params, selector);
}
if (nsigner_client_call(client, "nostr_get_public_key", params, &result) != NOSTR_SUCCESS) {
params = NULL;
@@ -393,6 +408,143 @@ static int test_local_derive_hmac_matches_reference(void) {
return ok;
}
/* The algorithm-based verbs are remote-only. The local backend must return
* NOSTR_ERROR_NOT_SUPPORTED for each of them. */
static int test_local_backend_not_supported(void) {
unsigned char privkey[32];
nostr_signer_t* signer = NULL;
int ok = 1;
int i;
char* out = NULL;
cJSON* info = NULL;
int valid = 0;
unsigned char msg[4] = { 0x68, 0x65, 0x6c, 0x6f }; /* "helo" */
for (i = 0; i < 32; i++) {
privkey[i] = (unsigned char)(i + 1);
}
signer = nostr_signer_local(privkey);
if (signer == NULL) {
return 0;
}
ok = ok && (nostr_signer_get_info(signer, &info) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_get_public_key_alg(signer, "secp256k1", 0, &out) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_sign(signer, "ed25519", 0, NULL, msg, 4, &out) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_verify(signer, "ed25519", 0, NULL, msg, 4, msg, 4, &valid) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_encapsulate(signer, "00", &out) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_decapsulate(signer, 0, "00", &out) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_derive_shared_secret(signer, 0, "00", &out) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_otp_encrypt(signer, "aGVsbG8=", NULL, &out) == NOSTR_ERROR_NOT_SUPPORTED);
ok = ok && (nostr_signer_otp_decrypt(signer, "blob", NULL, &out) == NOSTR_ERROR_NOT_SUPPORTED);
{
cJSON* evt = cJSON_CreateObject();
cJSON_AddNumberToObject(evt, "kind", 1);
ok = ok && (nostr_signer_mine_event(signer, evt, 4, 0, 1, &info) == NOSTR_ERROR_NOT_SUPPORTED);
cJSON_Delete(evt);
}
nostr_signer_free(signer);
return ok;
}
/* Remote wrappers build correct wire requests. Uses a mock fds transport:
* the child asserts the method + key fields and returns a canned response.
* nostr_signer_nsigner_fds takes read_fd/write_fd and builds its own
* transport internally, so we pass the socketpair fds directly. */
static int test_remote_wrappers_build_correct_wire(void) {
int fds[2] = { -1, -1 };
pid_t pid = -1;
int status = 0;
int ok = 0;
nostr_signer_t* signer = NULL;
char* result_str = NULL;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
return 0;
}
pid = fork();
if (pid < 0) {
goto cleanup;
}
if (pid == 0) {
unsigned char hdr[4];
unsigned int req_len = 0;
char* req = NULL;
const char* response = "{\"id\":\"1\",\"result\":\"{\\\"signature\\\":\\\"aabb\\\",\\\"algorithm\\\":\\\"ed25519\\\",\\\"key_id\\\":\\\"ccdd\\\"}\"}";
unsigned int res_len = (unsigned int)strlen(response);
unsigned char out_hdr[4];
close(fds[0]);
if (read_exact_fd(fds[1], hdr, sizeof(hdr)) != 0) { _exit(2); }
req_len = ((unsigned int)hdr[0] << 24) | ((unsigned int)hdr[1] << 16) |
((unsigned int)hdr[2] << 8) | (unsigned int)hdr[3];
if (req_len == 0 || req_len > 65536U) { _exit(3); }
req = (char*)malloc((size_t)req_len + 1U);
if (req == NULL) { _exit(4); }
if (read_exact_fd(fds[1], (unsigned char*)req, req_len) != 0) { free(req); _exit(5); }
req[req_len] = '\0';
/* nostr_signer_sign(ed25519, 0, NULL, "helo") must emit:
* method "sign", positional msg hex "68656c6f",
* options algorithm=ed25519, index=0, no scheme field. */
if (strstr(req, "\"method\":\"sign\"") == NULL) { free(req); _exit(6); }
if (strstr(req, "\"68656c6f\"") == NULL) { free(req); _exit(7); }
if (strstr(req, "\"algorithm\":\"ed25519\"") == NULL) { free(req); _exit(8); }
if (strstr(req, "\"index\":0") == NULL) { free(req); _exit(9); }
if (strstr(req, "\"scheme\"") != NULL) { free(req); _exit(10); } /* scheme omitted when NULL */
free(req);
out_hdr[0] = (unsigned char)((res_len >> 24) & 0xFFU);
out_hdr[1] = (unsigned char)((res_len >> 16) & 0xFFU);
out_hdr[2] = (unsigned char)((res_len >> 8) & 0xFFU);
out_hdr[3] = (unsigned char)(res_len & 0xFFU);
if (write_full_fd(fds[1], out_hdr, sizeof(out_hdr)) != 0) { _exit(11); }
if (write_full_fd(fds[1], (const unsigned char*)response, res_len) != 0) { _exit(12); }
close(fds[1]);
_exit(0);
}
close(fds[1]);
fds[1] = -1;
/* The high-level signer takes ownership of the fds via its internal
* transport. Pass the client side of the socketpair. */
signer = nostr_signer_nsigner_fds(fds[0], fds[0], NULL, 3000);
if (signer == NULL) {
goto cleanup;
}
fds[0] = -1; /* owned by signer now */
{
unsigned char msg[4] = { 0x68, 0x65, 0x6c, 0x6f };
if (nostr_signer_sign(signer, "ed25519", 0, NULL, msg, 4, &result_str) != NOSTR_SUCCESS) {
goto cleanup;
}
if (result_str == NULL || strstr(result_str, "aabb") == NULL) {
goto cleanup;
}
}
if (waitpid(pid, &status, 0) < 0) { goto cleanup; }
pid = -1;
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { goto cleanup; }
ok = 1;
cleanup:
if (result_str) free(result_str);
if (signer) nostr_signer_free(signer);
if (fds[0] >= 0) close(fds[0]);
if (fds[1] >= 0) close(fds[1]);
if (pid > 0) { (void)waitpid(pid, &status, 0); }
return ok;
}
int main(void) {
#if !defined(NOSTR_ENABLE_NSIGNER_CLIENT)
printf("nsigner client disabled in this build; skipping\n");
@@ -404,6 +556,8 @@ int main(void) {
run_result("/proc/net/unix discovery parser no-crash", test_unix_discovery_no_crash());
run_result("fds transport client round-trip over framed fd pair", test_fds_transport_round_trip_via_client());
run_result("local signer derive_hmac matches reference HMAC-SHA256", test_local_derive_hmac_matches_reference());
run_result("local backend returns NOT_SUPPORTED for algorithm verbs", test_local_backend_not_supported());
run_result("remote wrappers build correct wire (sign ed25519)", test_remote_wrappers_build_correct_wire());
printf("\nTotal: %d Passed: %d Failed: %d\n", test_count, passed_count, test_count - passed_count);
return (test_count == passed_count) ? 0 : 1;