From d575e8cdbc8cc263ea3b9274c6a965ff3630a185 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 6 Aug 2026 09:54:50 -0400 Subject: [PATCH] Add 10 high-level algorithm-verb wrappers to nostr_signer_t MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- nostr_core/NSIGNER_INTEGRATION.md | 58 +++ nostr_core/nostr_common.h | 1 + nostr_core/nostr_signer.c | 675 ++++++++++++++++++++++++++++++ nostr_core/nostr_signer.h | 89 ++++ tests/nsigner_client_test.c | 139 ++++++ 5 files changed, 962 insertions(+) diff --git a/nostr_core/NSIGNER_INTEGRATION.md b/nostr_core/NSIGNER_INTEGRATION.md index 52aabc35..d33f43f6 100644 --- a/nostr_core/NSIGNER_INTEGRATION.md +++ b/nostr_core/NSIGNER_INTEGRATION.md @@ -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`) diff --git a/nostr_core/nostr_common.h b/nostr_core/nostr_common.h index 575ac4b3..1252ec56 100644 --- a/nostr_core/nostr_common.h +++ b/nostr_core/nostr_common.h @@ -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 diff --git a/nostr_core/nostr_signer.c b/nostr_core/nostr_signer.c index ed67942f..d1438dde 100644 --- a/nostr_core/nostr_signer.c +++ b/nostr_core/nostr_signer.c @@ -592,6 +592,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]) { @@ -766,6 +1243,204 @@ int nostr_signer_derive_hmac(nostr_signer_t* signer, return NOSTR_ERROR_INVALID_INPUT; } +/* ---- Public dispatch for the algorithm-based verbs ---- */ + +int nostr_signer_get_info(nostr_signer_t* signer, cJSON** info_out) { + if (!signer || !info_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_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) static nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role) { nsigner_client_t* client = NULL; diff --git a/nostr_core/nostr_signer.h b/nostr_core/nostr_signer.h index 6ced519c..1b0e8050 100644 --- a/nostr_core/nostr_signer.h +++ b/nostr_core/nostr_signer.h @@ -47,6 +47,95 @@ 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":"","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":"","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":"","shared_secret":"","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":"","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":"","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) 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); diff --git a/tests/nsigner_client_test.c b/tests/nsigner_client_test.c index 483662c8..ae20a8f7 100644 --- a/tests/nsigner_client_test.c +++ b/tests/nsigner_client_test.c @@ -408,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"); @@ -419,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;