Add nostr_signer_derive_hmac() (HMAC-SHA256 via local + nsigner derive verb) and ttyUSB serial enumeration
Rescued from uncommitted work in sovereign_browser/nostr_core_lib: - nostr_signer_derive_hmac(): deterministic MAC over caller data using the signer's secp256k1 private key. Local backend computes HMAC-SHA256 directly; remote nsigner backend calls the 'derive' verb (algorithm:'secp256k1'). - nsigner_transport_list_serial(): enumerate /dev/ttyUSB* (FTDI/CH340/CP210x/ PL2303 USB-serial adapters) in addition to /dev/ttyACM* CDC-ACM devices. - tests/nsigner_client_test.c: local derive_hmac reference-match test.
This commit is contained in:
@@ -284,6 +284,26 @@ static int signer_local_nip44_decrypt(nostr_signer_t* signer,
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
unsigned char mac[32];
|
||||
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_hmac_sha256(signer->u.local.private_key, 32,
|
||||
(const unsigned char*)data, strlen(data),
|
||||
mac) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(mac, 32, out_digest_hex);
|
||||
memset(mac, 0, sizeof(mac));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
#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) {
|
||||
@@ -505,6 +525,74 @@ static int signer_remote_nip44_decrypt(nostr_signer_t* signer,
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip44_decrypt", peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
cJSON* params;
|
||||
cJSON* opts = NULL;
|
||||
cJSON* result = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
cJSON* digest_item = NULL;
|
||||
const char* digest_str = NULL;
|
||||
int rc;
|
||||
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
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). */
|
||||
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);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "derive", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* result is a JSON string: {"algorithm":"secp256k1","key_id":"...","digest":"<64hex>"} */
|
||||
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;
|
||||
}
|
||||
digest_item = cJSON_GetObjectItemCaseSensitive(parsed, "digest");
|
||||
if (!cJSON_IsString(digest_item) || digest_item->valuestring == NULL ||
|
||||
strlen(digest_item->valuestring) != 64) {
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
digest_str = digest_item->valuestring;
|
||||
memcpy(out_digest_hex, digest_str, 64);
|
||||
out_digest_hex[64] = '\0';
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]) {
|
||||
@@ -659,6 +747,25 @@ int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_derive_hmac(signer, data, out_digest_hex);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_derive_hmac(signer, data, out_digest_hex);
|
||||
}
|
||||
#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;
|
||||
|
||||
@@ -19,6 +19,16 @@ void nostr_signer_free(nostr_signer_t* signer);
|
||||
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);
|
||||
|
||||
/* Derive a deterministic MAC over caller-supplied data using the signer's
|
||||
* secp256k1 private key: out = HMAC-SHA256(privkey, data).
|
||||
* data is a NUL-terminated UTF-8 string. out_digest_hex receives 64 lowercase
|
||||
* hex chars + NUL. For the local backend this computes directly; for the
|
||||
* nsigner remote backend it calls the "derive" verb (algorithm:"secp256k1").
|
||||
* Returns NOSTR_SUCCESS or an error code. */
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]);
|
||||
|
||||
/* Encryption verbs (for parity with upcoming remote backends) */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
|
||||
@@ -1021,8 +1021,20 @@ int nsigner_transport_list_serial(char paths[][64], int max_paths) {
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
int n;
|
||||
int is_serial = 0;
|
||||
|
||||
if (strncmp(ent->d_name, "ttyACM", 6) != 0) {
|
||||
/* USB CDC-ACM devices (e.g. n_signer firmware using TinyUSB CDC). */
|
||||
if (strncmp(ent->d_name, "ttyACM", 6) == 0) {
|
||||
is_serial = 1;
|
||||
}
|
||||
/* USB-serial adapters (FTDI / CH340 / CP210x / PL2303). These show up
|
||||
* as /dev/ttyUSB* and speak the same 115200 8N1 framing as CDC-ACM,
|
||||
* so they are valid n_signer serial transports too. */
|
||||
if (strncmp(ent->d_name, "ttyUSB", 6) == 0) {
|
||||
is_serial = 1;
|
||||
}
|
||||
|
||||
if (!is_serial) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../nostr_core/nsigner_client.h"
|
||||
#include "../nostr_core/utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "../nostr_core/nostr_signer.h"
|
||||
|
||||
static int test_count = 0;
|
||||
static int passed_count = 0;
|
||||
@@ -332,6 +333,66 @@ cleanup:
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Local-signer derive_hmac: HMAC-SHA256(privkey, data) via the high-level API
|
||||
* must match nostr_hmac_sha256 computed directly with the same privkey. */
|
||||
static int test_local_derive_hmac_matches_reference(void) {
|
||||
unsigned char privkey[32];
|
||||
nostr_signer_t* signer = NULL;
|
||||
char got_hex[65];
|
||||
unsigned char ref_mac[32];
|
||||
char ref_hex[65];
|
||||
int ok = 0;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 32; i++) {
|
||||
privkey[i] = (unsigned char)(i + 1);
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(privkey);
|
||||
if (signer == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nostr_signer_derive_hmac(signer, "sovereign-browser/bookmarks-folder-id-v1:Work/Projects", got_hex) != NOSTR_SUCCESS) {
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nostr_hmac_sha256(privkey, 32,
|
||||
(const unsigned char*)"sovereign-browser/bookmarks-folder-id-v1:Work/Projects",
|
||||
strlen("sovereign-browser/bookmarks-folder-id-v1:Work/Projects"),
|
||||
ref_mac) != 0) {
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
nostr_bytes_to_hex(ref_mac, 32, ref_hex);
|
||||
|
||||
ok = (strlen(got_hex) == 64 && strcmp(got_hex, ref_hex) == 0);
|
||||
|
||||
/* Determinism: same input -> same digest. */
|
||||
{
|
||||
char got2_hex[65];
|
||||
if (nostr_signer_derive_hmac(signer, "sovereign-browser/bookmarks-folder-id-v1:Work/Projects", got2_hex) == NOSTR_SUCCESS) {
|
||||
ok = ok && (strcmp(got_hex, got2_hex) == 0);
|
||||
} else {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Opaqueness: different input -> different digest. */
|
||||
{
|
||||
char other_hex[65];
|
||||
if (nostr_signer_derive_hmac(signer, "sovereign-browser/bookmarks-folder-id-v1:Personal", other_hex) == NOSTR_SUCCESS) {
|
||||
ok = ok && (strcmp(got_hex, other_hex) != 0);
|
||||
} else {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
|
||||
nostr_signer_free(signer);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
#if !defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
printf("nsigner client disabled in this build; skipping\n");
|
||||
@@ -342,6 +403,7 @@ int main(void) {
|
||||
run_result("auth body hash matches sha256(compact params)", test_body_hash_matches_sha256_compact_params());
|
||||
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());
|
||||
|
||||
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