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:
Laan Tungir
2026-07-28 06:45:05 -04:00
parent 9240f44ea3
commit 024b490ac1
4 changed files with 192 additions and 1 deletions
+107
View File
@@ -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;
+10
View File
@@ -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,
+13 -1
View File
@@ -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;
}
+62
View File
@@ -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;