Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7c04dc49 | ||
|
|
c91670f7da | ||
|
|
71a72dc7ed | ||
|
|
d575e8cdbc | ||
|
|
b235bd6585 |
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user