#include #include #include #include #include #include #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "mbedtls/sha256.h" #include "mbedtls/md.h" #include "psa/crypto.h" #include "cJSON.h" #include "secp256k1.h" #include "secp256k1_extrakeys.h" #include "secp256k1_schnorrsig.h" #include "bech32.h" #include "buttons.h" #include "display.h" #include "key_derivation.h" #include "pq_crypto_firmware.h" #include "mnemonic.h" #include "usb_transport.h" #include "ui.h" #include "secure_mem.h" #include "nip004.h" #include "nip044.h" #include "nostr_common.h" #include "utils.h" #define FIRMWARE_VERSION "0.0.2" #ifndef GIT_HASH #define GIT_HASH "dev" #endif static const char *TAG = "nsigner"; /* Debug aid: skip startup menu and auto-generate a fresh mnemonic each boot. */ #define CYD_DEBUG_AUTO_GENERATE 0 #define APPROVAL_TIMEOUT_MS 30000 typedef enum { APPROVAL_DENY = 0, APPROVAL_ONCE = 1, APPROVAL_ALWAYS = 2, APPROVAL_TIMEOUT = 3, } approval_decision_t; static bool s_always_allow_sign = false; #define AUTH_REQUIRED 1 #define AUTH_KIND 27235 #define AUTH_NONCE_CACHE 32 static uint8_t s_nonce_cache[AUTH_NONCE_CACHE][32]; static int s_nonce_count = 0; static int s_nonce_next = 0; #define AUTH_ERR_ENVELOPE_MALFORMED 2010 #define AUTH_ERR_BODY_MISMATCH 2011 #define AUTH_ERR_SIGNATURE_INVALID 2012 #define AUTH_ERR_KIND_INVALID 2013 #define AUTH_ERR_ENVELOPE_REQUIRED 2014 #define AUTH_ERR_REPLAY_DETECTED 2017 /* Keep large buffers off the main task stack (ESP32 default stack is limited). */ static char s_mnemonic[256]; static uint8_t s_seed[64]; static uint8_t s_privkey[32]; static uint8_t s_pubkey[32]; static char s_npub[128]; static uint8_t s_frame_buf[4096]; static char s_pubkey_hex[65]; /* PQ pubkeys are large (ml-dsa-65 = 1952 B -> 3904 hex + JSON wrapper). * ml-kem-768 pubkey = 1184 B -> 2368 hex. ml-dsa-65 sig = 3309 B -> 6618 hex. * Size for the worst case (sign result with ml-dsa-65 signature). */ static char s_response_buf[8192]; static char s_signed_event_buf[1600]; static char s_encrypt_buf[1600]; /* Static PQ key buffers (kept off the stack; zeroized after use). */ static uint8_t s_pq_pk[FW_ML_DSA_65_PUBKEY_LEN]; /* large enough for any PQ pubkey */ static uint8_t s_pq_sk[FW_ML_DSA_65_PRIVKEY_LEN]; /* large enough for any PQ privkey */ static uint8_t s_pq_sig[FW_SLH_DSA_128S_SIG_LEN]; /* large enough for any PQ sig */ static char s_pq_hex[(FW_SLH_DSA_128S_SIG_LEN * 2) + 1]; /* hex for any PQ blob */ /* OTP pad state: derived from the mnemonic seed, monotonic offset. */ static uint8_t s_otp_pad[1024]; static size_t s_otp_pad_len = 0; static size_t s_otp_offset = 0; /* ---- Algorithm-based API (mirrors src/dispatcher.c) ---- */ typedef enum { FW_ALG_SECP256K1 = 0, FW_ALG_ED25519, FW_ALG_X25519, FW_ALG_ML_DSA_65, FW_ALG_SLH_DSA_128S, FW_ALG_ML_KEM_768, FW_ALG_OTP, FW_ALG_UNKNOWN } fw_alg_t; /* Algorithm-based verb names (canonical, no legacy aliases). */ #define VERB_SIGN "sign" #define VERB_VERIFY "verify" #define VERB_ENCAPSULATE "encapsulate" #define VERB_DECAPSULATE "decapsulate" #define VERB_DERIVE_SHARED "derive_shared_secret" #define VERB_DERIVE "derive" #define VERB_GET_PUBLIC_KEY "get_public_key" #define VERB_ENCRYPT "encrypt" #define VERB_DECRYPT "decrypt" /* Nostr protocol verbs (secp256k1 NIP-06, nostr_index selector). */ #define VERB_NOSTR_GET_PUBLIC_KEY "nostr_get_public_key" #define VERB_NOSTR_SIGN_EVENT "nostr_sign_event" #define VERB_NOSTR_MINE_EVENT "nostr_mine_event" #define VERB_NOSTR_NIP04_ENCRYPT "nostr_nip04_encrypt" #define VERB_NOSTR_NIP04_DECRYPT "nostr_nip04_decrypt" #define VERB_NOSTR_NIP44_ENCRYPT "nostr_nip44_encrypt" #define VERB_NOSTR_NIP44_DECRYPT "nostr_nip44_decrypt" /* JSON-RPC error codes (match README.md §4.2). */ #define ERR_INVALID_REQUEST -32600 #define ERR_METHOD_NOT_FOUND -32601 #define ERR_INVALID_PARAMS -32602 #define ERR_INTERNAL -32603 #define ERR_DENIED_BY_USER -32000 #define ERR_APPROVAL_TIMEOUT -32001 #define ERR_MINING_FAILED 1008 #define ERR_NOT_YET_IMPLEMENTED 1009 #define ERR_ALG_NOT_SUPPORTED 1010 static int transport_init(void) { return usb_transport_init(); } /* ---- Algorithm helpers ---- */ /* Forward decl: defined later in this file. */ static void bytes_to_hex(const uint8_t *in, size_t in_len, char *out, size_t out_len); static const char *fw_alg_name(fw_alg_t alg) { switch (alg) { case FW_ALG_SECP256K1: return "secp256k1"; case FW_ALG_ED25519: return "ed25519"; case FW_ALG_X25519: return "x25519"; case FW_ALG_ML_DSA_65: return "ml-dsa-65"; case FW_ALG_SLH_DSA_128S: return "slh-dsa-128s"; case FW_ALG_ML_KEM_768: return "ml-kem-768"; case FW_ALG_OTP: return "otp"; default: return "unknown"; } } static fw_alg_t fw_alg_from_name(const char *name) { if (name == NULL) { return FW_ALG_UNKNOWN; } if (strcmp(name, "secp256k1") == 0) return FW_ALG_SECP256K1; if (strcmp(name, "ed25519") == 0) return FW_ALG_ED25519; if (strcmp(name, "x25519") == 0) return FW_ALG_X25519; if (strcmp(name, "ml-dsa-65") == 0) return FW_ALG_ML_DSA_65; if (strcmp(name, "slh-dsa-128s") == 0) return FW_ALG_SLH_DSA_128S; if (strcmp(name, "ml-kem-768") == 0) return FW_ALG_ML_KEM_768; if (strcmp(name, "otp") == 0) return FW_ALG_OTP; return FW_ALG_UNKNOWN; } /* Parse the trailing options object from a params array. * Returns 0 on success (options may be absent -> defaults), -1 on malformed. */ static int parse_options_from_params(cJSON *params, cJSON **out_options) { int n; cJSON *last; if (out_options == NULL) { return -1; } *out_options = NULL; if (params == NULL || !cJSON_IsArray(params)) { return 0; } n = cJSON_GetArraySize(params); if (n <= 0) { return 0; } last = cJSON_GetArrayItem(params, n - 1); if (last != NULL && cJSON_IsObject(last)) { *out_options = last; } return 0; } /* Parse algorithm + index from the options object. * algorithm is required for algorithm-based verbs; index defaults to 0. * Returns 0 on success, -1 on malformed/missing algorithm. */ static int parse_algorithm_from_options(cJSON *options, fw_alg_t *out_alg, uint32_t *out_index) { cJSON *alg_item; cJSON *idx_item; const char *alg_str; if (out_alg == NULL || out_index == NULL) { return -1; } *out_alg = FW_ALG_UNKNOWN; *out_index = 0; if (options == NULL || !cJSON_IsObject(options)) { return -1; } alg_item = cJSON_GetObjectItemCaseSensitive(options, "algorithm"); if (!cJSON_IsString(alg_item) || alg_item->valuestring == NULL) { return -1; } alg_str = alg_item->valuestring; *out_alg = fw_alg_from_name(alg_str); if (*out_alg == FW_ALG_UNKNOWN) { return -1; } idx_item = cJSON_GetObjectItemCaseSensitive(options, "index"); if (idx_item != NULL && cJSON_IsNumber(idx_item)) { if (idx_item->valueint < 0) { return -1; } *out_index = (uint32_t)idx_item->valueint; } return 0; } /* ---- Enforcement matrix (README.md §4.3) ---- * Returns 0 if (verb, alg) is allowed, ERR_ALG_NOT_SUPPORTED otherwise. */ static int enforce_alg_verb(fw_alg_t alg, const char *verb) { if (verb == NULL) { return ERR_ALG_NOT_SUPPORTED; } if (strcmp(verb, VERB_GET_PUBLIC_KEY) == 0) { /* all key-deriving algorithms (otp does not derive -> excluded) */ if (alg == FW_ALG_SECP256K1 || alg == FW_ALG_ED25519 || alg == FW_ALG_X25519 || alg == FW_ALG_ML_DSA_65 || alg == FW_ALG_SLH_DSA_128S || alg == FW_ALG_ML_KEM_768) { return 0; } return ERR_ALG_NOT_SUPPORTED; } if (strcmp(verb, VERB_SIGN) == 0 || strcmp(verb, VERB_VERIFY) == 0) { if (alg == FW_ALG_SECP256K1 || alg == FW_ALG_ED25519 || alg == FW_ALG_ML_DSA_65 || alg == FW_ALG_SLH_DSA_128S) { return 0; } return ERR_ALG_NOT_SUPPORTED; } if (strcmp(verb, VERB_ENCAPSULATE) == 0 || strcmp(verb, VERB_DECAPSULATE) == 0) { return (alg == FW_ALG_ML_KEM_768) ? 0 : ERR_ALG_NOT_SUPPORTED; } if (strcmp(verb, VERB_DERIVE_SHARED) == 0) { return (alg == FW_ALG_X25519) ? 0 : ERR_ALG_NOT_SUPPORTED; } if (strcmp(verb, VERB_DERIVE) == 0) { return (alg == FW_ALG_SECP256K1) ? 0 : ERR_ALG_NOT_SUPPORTED; } if (strcmp(verb, VERB_ENCRYPT) == 0 || strcmp(verb, VERB_DECRYPT) == 0) { return (alg == FW_ALG_OTP) ? 0 : ERR_ALG_NOT_SUPPORTED; } return ERR_ALG_NOT_SUPPORTED; } /* ---- Structured result builder ---- * Returns a JSON string like: * {"algorithm":"","key_id":"<16hex>","":""} * key_id may be NULL/empty (omitted). Caller owns the returned string. */ static char *build_alg_result_json(const char *alg_name, const char *key_id_16, const char *field_name, const char *field_value) { cJSON *obj = NULL; char *out = NULL; if (alg_name == NULL || field_name == NULL || field_value == NULL) { return NULL; } obj = cJSON_CreateObject(); if (obj == NULL) { return NULL; } cJSON_AddStringToObject(obj, "algorithm", alg_name); if (key_id_16 != NULL && key_id_16[0] != '\0') { cJSON_AddStringToObject(obj, "key_id", key_id_16); } cJSON_AddStringToObject(obj, field_name, field_value); out = cJSON_PrintUnformatted(obj); cJSON_Delete(obj); return out; } /* Derive the key_id (first 16 hex chars of the pubkey) into out[17]. */ static void derive_key_id_from_hex_pubkey(const char *pubkey_hex, char out[17]) { if (pubkey_hex == NULL || out == NULL) { return; } if (strlen(pubkey_hex) >= 16) { memcpy(out, pubkey_hex, 16); out[16] = '\0'; } else { out[0] = '\0'; } } /* ---- Unified algorithm key derivation ---- * * Derives the keypair for (alg, index) from s_seed. For classical algorithms * fills privkey[32] + pubkey[32] and pubkey_hex[65]. For PQ algorithms fills * the static s_pq_pk / s_pq_sk buffers and pubkey_hex (full PQ pubkey hex). * * For ml-kem-768 the "privkey" concept is the PQ secret key; callers that need * the privkey bytes should read s_pq_sk directly. * * Returns 0 on success, -1 on error. pubkey_hex_out must be large enough for * the algorithm's pubkey (2*FW_ML_DSA_65_PUBKEY_LEN+1 worst case -> use s_pq_hex). */ static int derive_alg_key(fw_alg_t alg, uint32_t index, uint8_t privkey_out[32], char *pubkey_hex_out, size_t pubkey_hex_out_len) { uint8_t pubkey32[32]; if (pubkey_hex_out == NULL || pubkey_hex_out_len < 65) { return -1; } pubkey_hex_out[0] = '\0'; switch (alg) { case FW_ALG_SECP256K1: if (privkey_out == NULL) { return -1; } if (derive_nostr_key_index(s_seed, index, privkey_out, pubkey32) != 0) { return -1; } bytes_to_hex(pubkey32, 32, pubkey_hex_out, pubkey_hex_out_len); secure_memzero(pubkey32, sizeof(pubkey32)); return 0; case FW_ALG_ED25519: if (privkey_out == NULL) { return -1; } if (derive_ed25519_key(s_seed, index, privkey_out, pubkey32) != 0) { return -1; } bytes_to_hex(pubkey32, 32, pubkey_hex_out, pubkey_hex_out_len); secure_memzero(pubkey32, sizeof(pubkey32)); return 0; case FW_ALG_X25519: if (privkey_out == NULL) { return -1; } if (derive_x25519_key(s_seed, index, privkey_out, pubkey32) != 0) { return -1; } bytes_to_hex(pubkey32, 32, pubkey_hex_out, pubkey_hex_out_len); secure_memzero(pubkey32, sizeof(pubkey32)); return 0; case FW_ALG_ML_DSA_65: if (derive_ml_dsa_65_key(s_seed, index, s_pq_pk, s_pq_sk) != 0) { return -1; } bytes_to_hex(s_pq_pk, FW_ML_DSA_65_PUBKEY_LEN, pubkey_hex_out, pubkey_hex_out_len); return 0; case FW_ALG_SLH_DSA_128S: ui_show_approval_prompt("system", 0, "deriving SLH-DSA key (slow)...", 30000); if (derive_slh_dsa_128s_key(s_seed, index, s_pq_pk, s_pq_sk) != 0) { return -1; } bytes_to_hex(s_pq_pk, FW_SLH_DSA_128S_PUBKEY_LEN, pubkey_hex_out, pubkey_hex_out_len); return 0; case FW_ALG_ML_KEM_768: if (derive_ml_kem_768_key(s_seed, index, s_pq_pk, s_pq_sk) != 0) { return -1; } bytes_to_hex(s_pq_pk, FW_ML_KEM_768_PUBKEY_LEN, pubkey_hex_out, pubkey_hex_out_len); return 0; default: return -1; } } static void zeroize_pq_keys(void) { secure_memzero(s_pq_pk, sizeof(s_pq_pk)); secure_memzero(s_pq_sk, sizeof(s_pq_sk)); secure_memzero(s_pq_sig, sizeof(s_pq_sig)); } static approval_decision_t wait_for_approval(uint32_t timeout_ms) { ui_approval_decision_t ui_decision = ui_wait_for_approval(timeout_ms); if (ui_decision == UI_APPROVAL_DENY) { return APPROVAL_DENY; } if (ui_decision == UI_APPROVAL_ONCE) { return APPROVAL_ONCE; } if (ui_decision == UI_APPROVAL_ALWAYS) { return APPROVAL_ALWAYS; } return APPROVAL_TIMEOUT; } static void show_approval_prompt_with_caller(const cJSON *event_in, const char *caller_pubkey_hex) { int kind = 1; const char *content = ""; const cJSON *kind_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "kind"); const cJSON *content_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "content"); if (cJSON_IsNumber(kind_item)) { kind = kind_item->valueint; } if (cJSON_IsString(content_item) && content_item->valuestring != NULL) { content = content_item->valuestring; } ui_show_approval_prompt(caller_pubkey_hex, kind, content, APPROVAL_TIMEOUT_MS); } static approval_decision_t prompt_non_sign_approval(const char *caller_pubkey_hex, int kind, const char *content) { if (s_always_allow_sign) { return APPROVAL_ALWAYS; } ui_show_approval_prompt(caller_pubkey_hex, kind, content ? content : "", APPROVAL_TIMEOUT_MS); approval_decision_t decision = wait_for_approval(APPROVAL_TIMEOUT_MS); if (decision == APPROVAL_ALWAYS) { s_always_allow_sign = true; } return decision; } static void show_idle_screen(void) { char version_line[24]; snprintf(version_line, sizeof(version_line), "v%s", FIRMWARE_VERSION); ui_show_idle(s_npub, s_mnemonic, version_line); } static int recv_frame_stdin(uint8_t *payload, size_t payload_max, size_t *out_len) { return cdc_recv_frame(payload, payload_max, out_len); } static int send_frame_stdout(const uint8_t *payload, size_t len) { return cdc_send_frame(payload, len); } static void bytes_to_hex(const uint8_t *in, size_t in_len, char *out, size_t out_len) { static const char hex[] = "0123456789abcdef"; size_t i; if (in == NULL || out == NULL || out_len < (in_len * 2U + 1U)) { return; } for (i = 0; i < in_len; ++i) { out[i * 2U] = hex[(in[i] >> 4) & 0x0F]; out[i * 2U + 1U] = hex[in[i] & 0x0F]; } out[in_len * 2U] = '\0'; } static int extract_json_id_token(const char *json, char *out, size_t out_len) { const char *id_pos; const char *p; size_t n = 0; if (json == NULL || out == NULL || out_len < 5) { return -1; } id_pos = strstr(json, "\"id\""); if (id_pos == NULL) { return -1; } p = strchr(id_pos, ':'); if (p == NULL) { return -1; } ++p; while (*p != '\0' && isspace((unsigned char)*p)) { ++p; } if (*p == '\"') { out[n++] = '\"'; ++p; while (*p != '\0' && *p != '\"' && n + 2 < out_len) { out[n++] = *p++; } if (*p != '\"') { return -1; } out[n++] = '\"'; out[n] = '\0'; return 0; } while (*p != '\0' && *p != ',' && *p != '}' && !isspace((unsigned char)*p) && n + 1 < out_len) { out[n++] = *p++; } if (n == 0) { return -1; } out[n] = '\0'; return 0; } static int extract_json_string_field(const char *json, const char *field, char *out, size_t out_len) { char key[48]; const char *pos; const char *p; size_t n = 0; if (json == NULL || field == NULL || out == NULL || out_len < 2) { return -1; } snprintf(key, sizeof(key), "\"%s\"", field); pos = strstr(json, key); if (pos == NULL) { return -1; } p = strchr(pos, ':'); if (p == NULL) { return -1; } ++p; while (*p != '\0' && isspace((unsigned char)*p)) { ++p; } if (*p != '\"') { return -1; } ++p; while (*p != '\0' && *p != '\"' && n + 1 < out_len) { out[n++] = *p++; } if (*p != '\"' || n == 0) { return -1; } out[n] = '\0'; return 0; } static int sha256_bytes(const uint8_t *in, size_t in_len, uint8_t out32[32]) { if (in == NULL || out32 == NULL) { return -1; } mbedtls_sha256(in, in_len, out32, 0); return 0; } static int build_signed_event_json(const cJSON *event_in, const char *pubkey_hex, const uint8_t privkey[32], char *out, size_t out_len) { const cJSON *kind_item; const cJSON *created_item; const cJSON *content_item; const cJSON *tags_item; int kind = 1; int64_t created_at = 0; const char *content = ""; cJSON *ser_arr = NULL; cJSON *result_obj = NULL; cJSON *tags_dup = NULL; char *ser_json = NULL; char *result_json = NULL; uint8_t id32[32] = {0}; uint8_t sig64[64] = {0}; char id_hex[65] = {0}; char sig_hex[129] = {0}; int rc = -1; if (event_in == NULL || pubkey_hex == NULL || privkey == NULL || out == NULL || out_len == 0) { return -1; } kind_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "kind"); created_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "created_at"); content_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "content"); tags_item = cJSON_GetObjectItemCaseSensitive((cJSON *)event_in, "tags"); if (cJSON_IsNumber(kind_item)) { kind = kind_item->valueint; } if (cJSON_IsNumber(created_item)) { created_at = (int64_t)created_item->valuedouble; } if (cJSON_IsString(content_item) && content_item->valuestring != NULL) { content = content_item->valuestring; } ser_arr = cJSON_CreateArray(); if (ser_arr == NULL) { goto cleanup; } cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(pubkey_hex)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber((double)created_at)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(kind)); if (cJSON_IsArray(tags_item)) { tags_dup = cJSON_Duplicate((cJSON *)tags_item, 1); } else { tags_dup = cJSON_CreateArray(); } if (tags_dup == NULL) { goto cleanup; } cJSON_AddItemToArray(ser_arr, tags_dup); tags_dup = NULL; cJSON_AddItemToArray(ser_arr, cJSON_CreateString(content)); ser_json = cJSON_PrintUnformatted(ser_arr); if (ser_json == NULL) { goto cleanup; } if (sha256_bytes((const uint8_t *)ser_json, strlen(ser_json), id32) != 0) { goto cleanup; } bytes_to_hex(id32, sizeof(id32), id_hex, sizeof(id_hex)); if (schnorr_sign32(privkey, id32, sig64) != 0) { goto cleanup; } bytes_to_hex(sig64, sizeof(sig64), sig_hex, sizeof(sig_hex)); result_obj = cJSON_CreateObject(); if (result_obj == NULL) { goto cleanup; } cJSON_AddStringToObject(result_obj, "id", id_hex); cJSON_AddStringToObject(result_obj, "pubkey", pubkey_hex); cJSON_AddNumberToObject(result_obj, "created_at", (double)created_at); cJSON_AddNumberToObject(result_obj, "kind", kind); if (cJSON_IsArray(tags_item)) { cJSON_AddItemToObject(result_obj, "tags", cJSON_Duplicate((cJSON *)tags_item, 1)); } else { cJSON_AddItemToObject(result_obj, "tags", cJSON_CreateArray()); } cJSON_AddStringToObject(result_obj, "content", content); cJSON_AddStringToObject(result_obj, "sig", sig_hex); result_json = cJSON_PrintUnformatted(result_obj); if (result_json == NULL) { goto cleanup; } if (strlen(result_json) + 1 > out_len) { goto cleanup; } memcpy(out, result_json, strlen(result_json) + 1); rc = 0; cleanup: if (ser_arr != NULL) { cJSON_Delete(ser_arr); } if (result_obj != NULL) { cJSON_Delete(result_obj); } if (tags_dup != NULL) { cJSON_Delete(tags_dup); } if (ser_json != NULL) { cJSON_free(ser_json); } if (result_json != NULL) { cJSON_free(result_json); } memset(id32, 0, sizeof(id32)); memset(sig64, 0, sizeof(sig64)); return rc; } static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) { size_t hex_len; size_t i; if (hex == NULL || out == NULL) { return -1; } hex_len = strlen(hex); if (hex_len != out_len * 2U) { return -1; } for (i = 0; i < out_len; ++i) { char hi = hex[i * 2U]; char lo = hex[i * 2U + 1U]; int hv, lv; if (hi >= '0' && hi <= '9') hv = hi - '0'; else if (hi >= 'a' && hi <= 'f') hv = 10 + (hi - 'a'); else if (hi >= 'A' && hi <= 'F') hv = 10 + (hi - 'A'); else return -1; if (lo >= '0' && lo <= '9') lv = lo - '0'; else if (lo >= 'a' && lo <= 'f') lv = 10 + (lo - 'a'); else if (lo >= 'A' && lo <= 'F') lv = 10 + (lo - 'A'); else return -1; out[i] = (uint8_t)((hv << 4) | lv); } return 0; } static int auth_replay_check_and_record(const uint8_t id32[32]) { int i; int slot; for (i = 0; i < s_nonce_count; ++i) { if (memcmp(s_nonce_cache[i], id32, 32) == 0) { return -1; } } slot = s_nonce_next; memcpy(s_nonce_cache[slot], id32, 32); s_nonce_next = (s_nonce_next + 1) % AUTH_NONCE_CACHE; if (s_nonce_count < AUTH_NONCE_CACHE) { s_nonce_count++; } return 0; } /* * Verify the auth envelope for an inbound JSON-RPC request. * * Returns: * 0 -> envelope valid, caller_pubkey_hex_out filled in (65 bytes incl. NUL) * >0 -> error code (matches AUTH_ERR_* constants) * * PoC-grade: no wall-clock skew check (no RTC). Replay protection via * in-memory nonce cache of recent auth event ids. */ static int auth_envelope_verify(cJSON *req, const char *method, char caller_pubkey_hex_out[65]) { cJSON *auth_obj = cJSON_GetObjectItemCaseSensitive(req, "auth"); cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params"); cJSON *id_item; cJSON *pk_item; cJSON *created_item; cJSON *kind_item; cJSON *tags_item; cJSON *content_item; cJSON *sig_item; cJSON *ser_arr = NULL; char *ser_json = NULL; char *params_json = NULL; char body_hash_hex[65]; uint8_t computed_id[32]; uint8_t expected_id[32]; uint8_t pubkey32[32]; uint8_t sig64[64]; secp256k1_xonly_pubkey xonly; secp256k1_context *ctx = NULL; int rc = AUTH_ERR_ENVELOPE_MALFORMED; int found_method = 0; int found_body = 0; cJSON *tag_entry; int kind_value; if (auth_obj == NULL || !cJSON_IsObject(auth_obj)) { return AUTH_ERR_ENVELOPE_REQUIRED; } id_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "id"); pk_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "pubkey"); created_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "created_at"); kind_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "kind"); tags_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "tags"); content_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "content"); sig_item = cJSON_GetObjectItemCaseSensitive(auth_obj, "sig"); if (!cJSON_IsString(id_item) || !cJSON_IsString(pk_item) || !cJSON_IsNumber(created_item) || !cJSON_IsNumber(kind_item) || !cJSON_IsArray(tags_item) || !cJSON_IsString(content_item) || !cJSON_IsString(sig_item)) { return AUTH_ERR_ENVELOPE_MALFORMED; } kind_value = kind_item->valueint; if (kind_value != AUTH_KIND) { return AUTH_ERR_KIND_INVALID; } if (hex_to_bytes(id_item->valuestring, expected_id, 32) != 0 || hex_to_bytes(pk_item->valuestring, pubkey32, 32) != 0 || hex_to_bytes(sig_item->valuestring, sig64, 64) != 0) { return AUTH_ERR_ENVELOPE_MALFORMED; } ser_arr = cJSON_CreateArray(); if (ser_arr == NULL) { return AUTH_ERR_ENVELOPE_MALFORMED; } cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(pk_item->valuestring)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(created_item->valuedouble)); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(kind_value)); cJSON_AddItemToArray(ser_arr, cJSON_Duplicate(tags_item, 1)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(content_item->valuestring)); ser_json = cJSON_PrintUnformatted(ser_arr); if (ser_json == NULL) { rc = AUTH_ERR_ENVELOPE_MALFORMED; goto done; } mbedtls_sha256((const uint8_t *)ser_json, strlen(ser_json), computed_id, 0); if (memcmp(computed_id, expected_id, 32) != 0) { rc = AUTH_ERR_ENVELOPE_MALFORMED; goto done; } ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); if (ctx == NULL) { rc = AUTH_ERR_SIGNATURE_INVALID; goto done; } if (!secp256k1_xonly_pubkey_parse(ctx, &xonly, pubkey32)) { rc = AUTH_ERR_SIGNATURE_INVALID; goto done; } if (!secp256k1_schnorrsig_verify(ctx, sig64, expected_id, 32, &xonly)) { rc = AUTH_ERR_SIGNATURE_INVALID; goto done; } { cJSON *params_for_hash = params != NULL ? params : cJSON_CreateArray(); params_json = cJSON_PrintUnformatted(params_for_hash); } if (params_json == NULL) { rc = AUTH_ERR_BODY_MISMATCH; goto done; } { uint8_t body_hash[32]; size_t i; static const char hex[] = "0123456789abcdef"; mbedtls_sha256((const uint8_t *)params_json, strlen(params_json), body_hash, 0); for (i = 0; i < 32; ++i) { body_hash_hex[i * 2U] = hex[(body_hash[i] >> 4) & 0x0F]; body_hash_hex[i * 2U + 1U] = hex[body_hash[i] & 0x0F]; } body_hash_hex[64] = '\0'; } cJSON_ArrayForEach(tag_entry, tags_item) { cJSON *k_item; cJSON *v_item; if (!cJSON_IsArray(tag_entry) || cJSON_GetArraySize(tag_entry) < 2) { continue; } k_item = cJSON_GetArrayItem(tag_entry, 0); v_item = cJSON_GetArrayItem(tag_entry, 1); if (!cJSON_IsString(k_item) || !cJSON_IsString(v_item)) { continue; } if (strcmp(k_item->valuestring, "nsigner_method") == 0 && strcmp(v_item->valuestring, method) == 0) { found_method = 1; } else if (strcmp(k_item->valuestring, "nsigner_body_hash") == 0 && strcasecmp(v_item->valuestring, body_hash_hex) == 0) { found_body = 1; } } if (!found_method || !found_body) { rc = AUTH_ERR_BODY_MISMATCH; goto done; } if (auth_replay_check_and_record(expected_id) != 0) { rc = AUTH_ERR_REPLAY_DETECTED; goto done; } memcpy(caller_pubkey_hex_out, pk_item->valuestring, 64); caller_pubkey_hex_out[64] = '\0'; rc = 0; done: if (ser_arr != NULL) { cJSON_Delete(ser_arr); } if (ser_json != NULL) { cJSON_free(ser_json); } if (params_json != NULL) { cJSON_free(params_json); } if (ctx != NULL) { secp256k1_context_destroy(ctx); } return rc; } static int parse_nostr_index_from_params(cJSON *params, uint32_t *out_index) { int params_count; cJSON *last_item; cJSON *idx_item; int idx; if (out_index == NULL) { return -1; } *out_index = 0; if (params == NULL || !cJSON_IsArray(params)) { return 0; } params_count = cJSON_GetArraySize(params); if (params_count <= 0) { return 0; } last_item = cJSON_GetArrayItem(params, params_count - 1); if (last_item == NULL || !cJSON_IsObject(last_item)) { return 0; } idx_item = cJSON_GetObjectItemCaseSensitive(last_item, "nostr_index"); if (idx_item == NULL) { return 0; } if (!cJSON_IsNumber(idx_item)) { return -1; } idx = idx_item->valueint; if (idx < 0) { return -1; } *out_index = (uint32_t)idx; return 0; } static int derive_request_key(uint32_t nostr_index, uint8_t privkey_out[32], char pubkey_hex_out[65]) { uint8_t pubkey[32]; if (privkey_out == NULL || pubkey_hex_out == NULL) { return -1; } if (derive_nostr_key_index(s_seed, nostr_index, privkey_out, pubkey) != 0) { return -1; } bytes_to_hex(pubkey, sizeof(pubkey), pubkey_hex_out, 65); secure_memzero(pubkey, sizeof(pubkey)); return 0; } static int parse_peer_and_message_params(cJSON *params, const char **peer_hex_out, const char **message_out, uint32_t *nostr_index_out) { cJSON *peer_item; cJSON *msg_item; if (params == NULL || !cJSON_IsArray(params) || peer_hex_out == NULL || message_out == NULL || nostr_index_out == NULL) { return -1; } if (cJSON_GetArraySize(params) < 2) { return -1; } peer_item = cJSON_GetArrayItem(params, 0); msg_item = cJSON_GetArrayItem(params, 1); if (!cJSON_IsString(peer_item) || peer_item->valuestring == NULL || !cJSON_IsString(msg_item) || msg_item->valuestring == NULL) { return -1; } if (parse_nostr_index_from_params(params, nostr_index_out) != 0) { return -1; } *peer_hex_out = peer_item->valuestring; *message_out = msg_item->valuestring; return 0; } static int apply_mnemonic_and_enter_working(const char *mnemonic) { if (mnemonic == NULL || mnemonic[0] == '\0') { return -1; } if (mnemonic_to_seed(mnemonic, s_seed) != 0) { ESP_ERROR_CHECK(display_draw_text(10, 50, "seed failed", RGB565_RED, RGB565_BLACK)); return -1; } if (derive_nostr_key_index(s_seed, 0u, s_privkey, s_pubkey) != 0) { ESP_ERROR_CHECK(display_draw_text(10, 50, "key failed", RGB565_RED, RGB565_BLACK)); secure_memzero(s_seed, sizeof(s_seed)); return -1; } if (npub_encode(s_pubkey, s_npub, sizeof(s_npub)) != 0) { ESP_ERROR_CHECK(display_draw_text(10, 50, "npub failed", RGB565_RED, RGB565_BLACK)); secure_memzero(s_seed, sizeof(s_seed)); secure_memzero(s_pubkey, sizeof(s_pubkey)); return -1; } bytes_to_hex(s_pubkey, sizeof(s_pubkey), s_pubkey_hex, sizeof(s_pubkey_hex)); printf("Derived npub[0]: %s\n", s_npub); printf("Derived pubkey hex[0]: %s\n", s_pubkey_hex); /* Derive the OTP pad from the mnemonic seed (no USB pad on CYD). * HKDF-SHA256(salt="nsigner-otp", ikm=seed, info="otp-pad") -> 1024 bytes. * The offset advances monotonically across encrypt/decrypt requests. */ { static const uint8_t otp_salt[12] = "nsigner-otp"; static const uint8_t otp_info[7] = "otp-pad"; if (nostr_hkdf(otp_salt, sizeof(otp_salt) - 1, s_seed, sizeof(s_seed), otp_info, sizeof(otp_info) - 1, s_otp_pad, sizeof(s_otp_pad)) != 0) { ESP_LOGE(TAG, "OTP pad derivation failed"); s_otp_pad_len = 0; } else { s_otp_pad_len = sizeof(s_otp_pad); } s_otp_offset = 0; } show_idle_screen(); secure_memzero(s_pubkey, sizeof(s_pubkey)); return 0; } /* ---- Base64 helpers (for OTP encode/decode) ---- */ static const char b64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static int b64_encode(const uint8_t *in, size_t in_len, char *out, size_t out_max) { size_t i = 0; size_t o = 0; size_t v; if (in == NULL || out == NULL) { return -1; } /* 4/3 expansion + padding + nul */ if (out_max < ((in_len + 2) / 3) * 4 + 1) { return -1; } while (i + 2 < in_len) { v = ((size_t)in[i] << 16) | ((size_t)in[i + 1] << 8) | (size_t)in[i + 2]; out[o++] = b64_chars[(v >> 18) & 0x3f]; out[o++] = b64_chars[(v >> 12) & 0x3f]; out[o++] = b64_chars[(v >> 6) & 0x3f]; out[o++] = b64_chars[v & 0x3f]; i += 3; } if (i < in_len) { v = (size_t)in[i] << 16; if (i + 1 < in_len) { v |= (size_t)in[i + 1] << 8; } out[o++] = b64_chars[(v >> 18) & 0x3f]; out[o++] = b64_chars[(v >> 12) & 0x3f]; out[o++] = (i + 1 < in_len) ? b64_chars[(v >> 6) & 0x3f] : '='; out[o++] = '='; } out[o] = '\0'; return 0; } static int b64_decode(const char *in, size_t in_len, uint8_t *out, size_t out_max, size_t *out_len) { int8_t dec[256]; size_t i = 0; size_t o = 0; int val = 0; int valb = -8; if (in == NULL || out == NULL || out_len == NULL) { return -1; } memset(dec, -1, sizeof(dec)); for (i = 0; i < 64; ++i) { dec[(uint8_t)b64_chars[i]] = (int8_t)i; } *out_len = 0; for (i = 0; i < in_len; ++i) { char c = in[i]; if (c == '=') { break; } if (dec[(uint8_t)c] == -1) { continue; } if (o >= out_max) { return -1; } val = (val << 6) | dec[(uint8_t)c]; valb += 6; if (valb >= 0) { out[o++] = (uint8_t)((val >> valb) & 0xff); valb -= 8; } } *out_len = o; return 0; } /* ---- x25519 ECDH shared secret via PSA crypto ---- */ static int x25519_ecdh(const uint8_t priv[32], const uint8_t peer_pub[32], uint8_t shared_out[32]) { psa_status_t status; psa_key_id_t key_id = 0; psa_key_attributes_t attrs = PSA_KEY_ATTRIBUTES_INIT; size_t shared_len = 0; psa_crypto_init(); psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_MONTGOMERY)); psa_set_key_bits(&attrs, 255); psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_DERIVE); psa_set_key_algorithm(&attrs, PSA_ALG_ECDH); status = psa_import_key(&attrs, priv, 32, &key_id); if (status != PSA_SUCCESS) { return -1; } status = psa_raw_key_agreement(PSA_ALG_ECDH, key_id, peer_pub, 32, shared_out, 32, &shared_len); psa_destroy_key(key_id); if (status != PSA_SUCCESS || shared_len != 32) { return -1; } return 0; } /* ---- secp256k1 ECDSA sign/verify (scheme:"ecdsa") ---- */ static int secp256k1_ecdsa_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig_out[64]) { secp256k1_context *ctx = NULL; secp256k1_ecdsa_signature sig; int rc = -1; ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); if (ctx == NULL) { return -1; } if (!secp256k1_ecdsa_sign(ctx, &sig, msg32, privkey, NULL, NULL)) { goto done; } secp256k1_ecdsa_signature_serialize_compact(ctx, sig_out, &sig); rc = 0; done: secp256k1_context_destroy(ctx); return rc; } static int secp256k1_ecdsa_verify32(const uint8_t msg32[32], const uint8_t sig64[64], const uint8_t pubkey32[32]) { secp256k1_context *ctx = NULL; secp256k1_ecdsa_signature sig; secp256k1_pubkey pub; int rc = -1; ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); if (ctx == NULL) { return -1; } if (!secp256k1_ecdsa_signature_parse_compact(ctx, &sig, sig64)) { goto done; } if (!secp256k1_ec_pubkey_parse(ctx, &pub, pubkey32, 33)) { /* try 33-byte compressed first; fall back to 32-byte x-only via tweak */ goto done; } rc = secp256k1_ecdsa_verify(ctx, &sig, msg32, &pub) ? 0 : -1; done: secp256k1_context_destroy(ctx); return rc; } /* Derive a 33-byte compressed secp256k1 pubkey from a 32-byte private key. */ static int secp256k1_priv_to_compressed_pub(const uint8_t priv[32], uint8_t pub33[33]) { secp256k1_context *ctx = NULL; secp256k1_pubkey pub; size_t olen = 33; int rc = -1; ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); if (ctx == NULL) { return -1; } if (!secp256k1_ec_pubkey_create(ctx, &pub, priv)) { goto done; } if (!secp256k1_ec_pubkey_serialize(ctx, pub33, &olen, &pub, SECP256K1_EC_COMPRESSED)) { goto done; } rc = 0; done: secp256k1_context_destroy(ctx); return rc; } /* ed25519 sign/verify are provided by key_derivation.{c,h} (PSA crypto). */ /* ---- Request handler: fills s_response_buf for the given parsed request ---- * * req : parsed JSON-RPC request object (caller frees) * method : the "method" string * id_token : raw JSON "id" token for the response (e.g. "\"1\"" or "null") * caller_hex : 64-hex caller pubkey from the auth envelope (or "anon") */ static void handle_request(cJSON *req, const char *method, const char *id_token, const char *caller_hex) { cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params"); char key_id[17]; /* ===================== Nostr verbs (secp256k1 NIP-06) ===================== */ if (strcmp(method, VERB_NOSTR_GET_PUBLIC_KEY) == 0) { uint32_t nostr_index = 0; uint8_t req_privkey[32]; char req_pubkey_hex[65]; cJSON *options = NULL; const char *fmt = NULL; memset(req_privkey, 0, sizeof(req_privkey)); memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex)); if (!cJSON_IsArray(params) || parse_nostr_index_from_params(params, &nostr_index) != 0 || derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); } else { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, "nostr_get_public_key"); if (d == APPROVAL_ONCE || d == APPROVAL_ALWAYS) { ui_add_event("nostr_get_public_key", caller_hex, "approved"); parse_options_from_params(params, &options); if (options != NULL) { cJSON *f = cJSON_GetObjectItemCaseSensitive(options, "format"); if (cJSON_IsString(f)) { fmt = f->valuestring; } } if (fmt != NULL && strcmp(fmt, "structured") == 0) { derive_key_id_from_hex_pubkey(req_pubkey_hex, key_id); char *res = build_alg_result_json("secp256k1", key_id, "public_key", req_pubkey_hex); if (res != NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"internal error\"}}", id_token, ERR_INTERNAL); } } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}", id_token, req_pubkey_hex); } } else if (d == APPROVAL_DENY) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"denied by user\"}}", id_token, ERR_DENIED_BY_USER); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"approval timeout\"}}", id_token, ERR_APPROVAL_TIMEOUT); } } secure_memzero(req_privkey, sizeof(req_privkey)); return; } if (strcmp(method, VERB_NOSTR_SIGN_EVENT) == 0) { cJSON *event_in = NULL; uint32_t nostr_index = 0; uint8_t req_privkey[32]; char req_pubkey_hex[65]; memset(req_privkey, 0, sizeof(req_privkey)); memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex)); if (cJSON_IsArray(params) && cJSON_GetArraySize(params) > 0) { event_in = cJSON_GetArrayItem(params, 0); } if (event_in != NULL && cJSON_IsObject(event_in) && parse_nostr_index_from_params(params, &nostr_index) == 0 && derive_request_key(nostr_index, req_privkey, req_pubkey_hex) == 0) { approval_decision_t d = APPROVAL_ALWAYS; int event_kind = 1; char event_extra[32]; const cJSON *kind_item = cJSON_GetObjectItemCaseSensitive(event_in, "kind"); if (cJSON_IsNumber(kind_item)) { event_kind = kind_item->valueint; } if (!s_always_allow_sign) { show_approval_prompt_with_caller(event_in, caller_hex); d = wait_for_approval(APPROVAL_TIMEOUT_MS); if (d == APPROVAL_ALWAYS) { s_always_allow_sign = true; } } if ((d == APPROVAL_ONCE || d == APPROVAL_ALWAYS) && build_signed_event_json(event_in, req_pubkey_hex, req_privkey, s_signed_event_buf, sizeof(s_signed_event_buf)) == 0) { snprintf(event_extra, sizeof(event_extra), "kind:%d", event_kind); ui_add_event("nostr_sign_event", caller_hex, event_extra); snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, s_signed_event_buf); } else if (d == APPROVAL_DENY) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"denied by user\"}}", id_token, ERR_DENIED_BY_USER); } else if (d == APPROVAL_TIMEOUT) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"approval timeout\"}}", id_token, ERR_APPROVAL_TIMEOUT); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"internal error\"}}", id_token, ERR_INTERNAL); } } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); } secure_memzero(req_privkey, sizeof(req_privkey)); return; } if (strcmp(method, VERB_NOSTR_MINE_EVENT) == 0) { cJSON *event_in = NULL; uint32_t nostr_index = 0; uint8_t req_privkey[32]; char req_pubkey_hex[65]; cJSON *options = NULL; int difficulty = 4; int timeout_sec = 30; uint32_t nonce = 0; int64_t deadline_ms; int mined = 0; memset(req_privkey, 0, sizeof(req_privkey)); memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex)); if (cJSON_IsArray(params) && cJSON_GetArraySize(params) > 0) { event_in = cJSON_GetArrayItem(params, 0); } parse_options_from_params(params, &options); if (options != NULL) { cJSON *d_item = cJSON_GetObjectItemCaseSensitive(options, "difficulty"); cJSON *t_item = cJSON_GetObjectItemCaseSensitive(options, "timeout_sec"); if (cJSON_IsNumber(d_item) && d_item->valueint > 0) { difficulty = d_item->valueint; } if (cJSON_IsNumber(t_item) && t_item->valueint > 0) { timeout_sec = t_item->valueint; } } if (event_in == NULL || !cJSON_IsObject(event_in) || parse_nostr_index_from_params(params, &nostr_index) != 0 || derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); secure_memzero(req_privkey, sizeof(req_privkey)); return; } { approval_decision_t d = APPROVAL_ALWAYS; if (!s_always_allow_sign) { show_approval_prompt_with_caller(event_in, caller_hex); d = wait_for_approval(APPROVAL_TIMEOUT_MS); if (d == APPROVAL_ALWAYS) { s_always_allow_sign = true; } } if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); secure_memzero(req_privkey, sizeof(req_privkey)); return; } } ui_show_approval_prompt(caller_hex, 0, "mining event...", (uint32_t)timeout_sec * 1000); deadline_ms = (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) + (int64_t)timeout_sec * 1000; /* Iterate nonce in a "nonce" tag until the event id has `difficulty` * leading zero bits. Single-threaded. */ while ((int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS) < deadline_ms) { cJSON *tags_item = cJSON_GetObjectItemCaseSensitive(event_in, "tags"); cJSON *nonce_tag = NULL; cJSON *ser_arr = NULL; char *ser_json = NULL; uint8_t id32[32]; char id_hex[65]; int leading_zeros = 0; /* Ensure a nonce tag exists as the last tag. */ if (cJSON_IsArray(tags_item)) { int tn = cJSON_GetArraySize(tags_item); if (tn > 0) { cJSON *last = cJSON_GetArrayItem(tags_item, tn - 1); if (cJSON_IsArray(last) && cJSON_GetArraySize(last) >= 1) { cJSON *k0 = cJSON_GetArrayItem(last, 0); if (cJSON_IsString(k0) && strcmp(k0->valuestring, "nonce") == 0) { nonce_tag = last; } } } } if (nonce_tag == NULL) { if (!cJSON_IsArray(tags_item)) { cJSON_AddItemToObject(event_in, "tags", (tags_item = cJSON_CreateArray())); } nonce_tag = cJSON_CreateArray(); cJSON_AddItemToArray(tags_item, nonce_tag); } /* (re)build nonce tag: ["nonce", "", ""] */ while (cJSON_GetArraySize(nonce_tag) > 0) { cJSON_DeleteItemFromArray(nonce_tag, cJSON_GetArraySize(nonce_tag) - 1); } cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("nonce")); { char nonce_str[16]; snprintf(nonce_str, sizeof(nonce_str), "%u", (unsigned int)nonce); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(nonce_str)); } { char diff_str[16]; snprintf(diff_str, sizeof(diff_str), "%d", difficulty); cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(diff_str)); } ser_arr = cJSON_CreateArray(); cJSON_AddItemToArray(ser_arr, cJSON_CreateNumber(0)); cJSON_AddItemToArray(ser_arr, cJSON_CreateString(req_pubkey_hex)); { const cJSON *c = cJSON_GetObjectItemCaseSensitive(event_in, "created_at"); cJSON_AddItemToArray(ser_arr, cJSON_IsNumber(c) ? cJSON_Duplicate(c, 1) : cJSON_CreateNumber(0)); } { const cJSON *k = cJSON_GetObjectItemCaseSensitive(event_in, "kind"); cJSON_AddItemToArray(ser_arr, cJSON_IsNumber(k) ? cJSON_Duplicate(k, 1) : cJSON_CreateNumber(1)); } { const cJSON *t = cJSON_GetObjectItemCaseSensitive(event_in, "tags"); cJSON_AddItemToArray(ser_arr, cJSON_IsArray(t) ? cJSON_Duplicate(t, 1) : cJSON_CreateArray()); } { const cJSON *ct = cJSON_GetObjectItemCaseSensitive(event_in, "content"); cJSON_AddItemToArray(ser_arr, cJSON_IsString(ct) ? cJSON_CreateString(ct->valuestring) : cJSON_CreateString("")); } ser_json = cJSON_PrintUnformatted(ser_arr); if (ser_json != NULL) { sha256_bytes((const uint8_t *)ser_json, strlen(ser_json), id32); /* count leading zero bits */ for (int b = 0; b < 32 && leading_zeros < difficulty; ++b) { uint8_t byte = id32[b]; if (byte == 0) { leading_zeros += 8; continue; } while ((byte & 0x80) == 0) { leading_zeros++; byte <<= 1; } break; } if (leading_zeros >= difficulty) { bytes_to_hex(id32, 32, id_hex, sizeof(id_hex)); if (build_signed_event_json(event_in, req_pubkey_hex, req_privkey, s_signed_event_buf, sizeof(s_signed_event_buf)) == 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, s_signed_event_buf); ui_add_event("nostr_mine_event", caller_hex, "mined"); mined = 1; } } } if (ser_arr) cJSON_Delete(ser_arr); if (ser_json) cJSON_free(ser_json); if (mined) { break; } nonce++; if ((nonce & 0xff) == 0) { ui_pump(); vTaskDelay(pdMS_TO_TICKS(1)); } } if (!mined) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"mining failed (timeout)\"}}", id_token, ERR_MINING_FAILED); } secure_memzero(req_privkey, sizeof(req_privkey)); return; } /* nip04 / nip44 encrypt + decrypt share the same param shape. */ if (strcmp(method, VERB_NOSTR_NIP04_ENCRYPT) == 0 || strcmp(method, VERB_NOSTR_NIP04_DECRYPT) == 0 || strcmp(method, VERB_NOSTR_NIP44_ENCRYPT) == 0 || strcmp(method, VERB_NOSTR_NIP44_DECRYPT) == 0) { const char *peer_hex = NULL; const char *message = NULL; uint32_t nostr_index = 0; uint8_t peer_pubkey[32]; uint8_t req_privkey[32]; char req_pubkey_hex[65]; int is_nip44 = (method[7] == '4'); /* nostr_nip44... vs nostr_nip04... */ int is_encrypt = (strstr(method, "encrypt") != NULL); int rc = -1; memset(peer_pubkey, 0, sizeof(peer_pubkey)); memset(req_privkey, 0, sizeof(req_privkey)); memset(req_pubkey_hex, 0, sizeof(req_pubkey_hex)); memset(s_encrypt_buf, 0, sizeof(s_encrypt_buf)); if (parse_peer_and_message_params(params, &peer_hex, &message, &nostr_index) != 0 || hex_to_bytes(peer_hex, peer_pubkey, sizeof(peer_pubkey)) != 0 || derive_request_key(nostr_index, req_privkey, req_pubkey_hex) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); } else { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, method); if (d == APPROVAL_ONCE || d == APPROVAL_ALWAYS) { ui_add_event(method, caller_hex, "approved"); if (is_nip44) { rc = is_encrypt ? nostr_nip44_encrypt(req_privkey, peer_pubkey, message, s_encrypt_buf, sizeof(s_encrypt_buf)) : nostr_nip44_decrypt(req_privkey, peer_pubkey, message, s_encrypt_buf, sizeof(s_encrypt_buf)); } else { rc = is_encrypt ? nostr_nip04_encrypt(req_privkey, peer_pubkey, message, s_encrypt_buf, sizeof(s_encrypt_buf)) : nostr_nip04_decrypt(req_privkey, peer_pubkey, message, s_encrypt_buf, sizeof(s_encrypt_buf)); } if (rc != NOSTR_SUCCESS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s failed\"}}", id_token, ERR_INTERNAL, method); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}", id_token, s_encrypt_buf); } } else if (d == APPROVAL_DENY) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"denied by user\"}}", id_token, ERR_DENIED_BY_USER); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"approval timeout\"}}", id_token, ERR_APPROVAL_TIMEOUT); } } secure_memzero(req_privkey, sizeof(req_privkey)); secure_memzero(peer_pubkey, sizeof(peer_pubkey)); return; } /* ===================== Algorithm-based verbs ===================== */ /* get_public_key (algorithm-based) */ if (strcmp(method, VERB_GET_PUBLIC_KEY) == 0) { cJSON *options = NULL; fw_alg_t alg; uint32_t index = 0; uint8_t req_privkey[32]; memset(req_privkey, 0, sizeof(req_privkey)); parse_options_from_params(params, &options); if (parse_algorithm_from_options(options, &alg, &index) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (enforce_alg_verb(alg, method) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"algorithm_not_supported_for_verb\"}}", id_token, ERR_ALG_NOT_SUPPORTED); return; } { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, "get_public_key"); if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); zeroize_pq_keys(); secure_memzero(req_privkey, sizeof(req_privkey)); return; } ui_add_event("get_public_key", caller_hex, fw_alg_name(alg)); if (derive_alg_key(alg, index, req_privkey, s_pq_hex, sizeof(s_pq_hex)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"key derivation failed\"}}", id_token, ERR_INTERNAL); } else { derive_key_id_from_hex_pubkey(s_pq_hex, key_id); char *res = build_alg_result_json(fw_alg_name(alg), key_id, "public_key", s_pq_hex); if (res != NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"internal error\"}}", id_token, ERR_INTERNAL); } } } zeroize_pq_keys(); secure_memzero(req_privkey, sizeof(req_privkey)); return; } /* sign + verify */ if (strcmp(method, VERB_SIGN) == 0 || strcmp(method, VERB_VERIFY) == 0) { cJSON *options = NULL; fw_alg_t alg; uint32_t index = 0; uint8_t req_privkey[32]; const char *msg_hex = NULL; const char *sig_hex = NULL; cJSON *msg_item = NULL; cJSON *sig_item = NULL; const char *scheme = "schnorr"; int is_verify = (strcmp(method, VERB_VERIFY) == 0); uint8_t *msg_bytes = NULL; size_t msg_len = 0; memset(req_privkey, 0, sizeof(req_privkey)); if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } msg_item = cJSON_GetArrayItem(params, 0); if (!cJSON_IsString(msg_item) || msg_item->valuestring == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } msg_hex = msg_item->valuestring; if (is_verify) { sig_item = cJSON_GetArrayItem(params, 1); if (!cJSON_IsString(sig_item) || sig_item->valuestring == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } sig_hex = sig_item->valuestring; } parse_options_from_params(params, &options); if (parse_algorithm_from_options(options, &alg, &index) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (enforce_alg_verb(alg, method) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"algorithm_not_supported_for_verb\"}}", id_token, ERR_ALG_NOT_SUPPORTED); return; } if (alg == FW_ALG_SECP256K1 && options != NULL) { cJSON *s = cJSON_GetObjectItemCaseSensitive(options, "scheme"); if (cJSON_IsString(s) && s->valuestring != NULL) { scheme = s->valuestring; } } /* decode message hex */ msg_len = strlen(msg_hex) / 2; if (msg_len == 0 || strlen(msg_hex) % 2 != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } msg_bytes = (uint8_t *)malloc(msg_len); if (msg_bytes == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"internal error\"}}", id_token, ERR_INTERNAL); return; } if (hex_to_bytes(msg_hex, msg_bytes, msg_len) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); free(msg_bytes); return; } { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, method); if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); free(msg_bytes); zeroize_pq_keys(); secure_memzero(req_privkey, sizeof(req_privkey)); return; } ui_add_event(method, caller_hex, fw_alg_name(alg)); if (derive_alg_key(alg, index, req_privkey, s_pq_hex, sizeof(s_pq_hex)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"key derivation failed\"}}", id_token, ERR_INTERNAL); } else { derive_key_id_from_hex_pubkey(s_pq_hex, key_id); if (!is_verify) { /* ---- sign ---- */ if (alg == FW_ALG_SECP256K1) { uint8_t msg32[32]; uint8_t sig64[64]; if (strcmp(scheme, "ecdsa") == 0) { sha256_bytes(msg_bytes, msg_len, msg32); if (secp256k1_ecdsa_sign32(req_privkey, msg32, sig64) == 0) { bytes_to_hex(sig64, 64, s_pq_hex, sizeof(s_pq_hex)); char *res = build_alg_result_json("secp256k1", key_id, "signature", s_pq_hex); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } secure_memzero(msg32, sizeof(msg32)); secure_memzero(sig64, sizeof(sig64)); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"sign failed\"}}", id_token, ERR_INTERNAL); } } else { /* schnorr: sign the 32-byte message directly (BIP-340) */ if (msg_len == 32 && schnorr_sign32(req_privkey, msg_bytes, sig64) == 0) { bytes_to_hex(sig64, 64, s_pq_hex, sizeof(s_pq_hex)); char *res = build_alg_result_json("secp256k1", key_id, "signature", s_pq_hex); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } secure_memzero(sig64, sizeof(sig64)); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"schnorr sign requires 32-byte message\"}}", id_token, ERR_INVALID_PARAMS); } } } else if (alg == FW_ALG_ED25519) { uint8_t sig64[64]; /* ed25519 signs the raw message (PureEdDSA, variable length) */ if (ed25519_sign_msg(req_privkey, msg_bytes, msg_len, sig64) == 0) { bytes_to_hex(sig64, 64, s_pq_hex, sizeof(s_pq_hex)); char *res = build_alg_result_json("ed25519", key_id, "signature", s_pq_hex); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } secure_memzero(sig64, sizeof(sig64)); } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"ed25519 sign failed\"}}", id_token, ERR_INTERNAL); } } else if (alg == FW_ALG_ML_DSA_65) { size_t siglen = 0; if (fw_pq_ml_dsa_65_sign(s_pq_sig, &siglen, msg_bytes, msg_len, s_pq_sk) == 0) { bytes_to_hex(s_pq_sig, siglen, s_pq_hex, sizeof(s_pq_hex)); char *res = build_alg_result_json("ml-dsa-65", key_id, "signature", s_pq_hex); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"ml-dsa-65 sign failed\"}}", id_token, ERR_INTERNAL); } } else if (alg == FW_ALG_SLH_DSA_128S) { size_t siglen = 0; ui_show_approval_prompt(caller_hex, 0, "SLH-DSA signing (slow)...", 60000); if (fw_pq_slh_dsa_128s_sign(s_pq_sig, &siglen, msg_bytes, msg_len, s_pq_sk) == 0) { bytes_to_hex(s_pq_sig, siglen, s_pq_hex, sizeof(s_pq_hex)); char *res = build_alg_result_json("slh-dsa-128s", key_id, "signature", s_pq_hex); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } } else { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"slh-dsa-128s sign failed\"}}", id_token, ERR_INTERNAL); } } } else { /* ---- verify ---- */ size_t sig_len = strlen(sig_hex) / 2; if (sig_len == 0 || strlen(sig_hex) % 2 != 0 || sig_len > sizeof(s_pq_sig)) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); } else if (hex_to_bytes(sig_hex, s_pq_sig, sig_len) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); } else { int valid = 0; if (alg == FW_ALG_SECP256K1) { uint8_t msg32[32]; if (strcmp(scheme, "ecdsa") == 0) { sha256_bytes(msg_bytes, msg_len, msg32); /* need compressed pubkey for ecdsa verify */ uint8_t pub33[33]; if (secp256k1_priv_to_compressed_pub(req_privkey, pub33) == 0 && secp256k1_ecdsa_verify32(msg32, s_pq_sig, pub33) == 0) { valid = 1; } secure_memzero(pub33, sizeof(pub33)); secure_memzero(msg32, sizeof(msg32)); } else { /* schnorr verify against x-only pubkey (s_pq_hex holds x-only) */ secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); secp256k1_xonly_pubkey xonly; uint8_t pub32[32]; if (ctx != NULL && hex_to_bytes(s_pq_hex, pub32, 32) == 0 && secp256k1_xonly_pubkey_parse(ctx, &xonly, pub32) && secp256k1_schnorrsig_verify(ctx, s_pq_sig, msg_bytes, msg_len, &xonly)) { valid = 1; } if (ctx) secp256k1_context_destroy(ctx); secure_memzero(pub32, sizeof(pub32)); } } else if (alg == FW_ALG_ED25519) { /* mbedtls ed25519 verify: sign/verify raw message */ /* re-derive pubkey already in s_pq_hex; parse and verify */ uint8_t pub32[32]; if (hex_to_bytes(s_pq_hex, pub32, 32) == 0) { valid = (ed25519_verify_msg(s_pq_sig, sig_len, msg_bytes, msg_len, pub32) == 0); } secure_memzero(pub32, sizeof(pub32)); } else if (alg == FW_ALG_ML_DSA_65) { valid = (fw_pq_ml_dsa_65_verify(s_pq_sig, sig_len, msg_bytes, msg_len, s_pq_pk) == 0); } else if (alg == FW_ALG_SLH_DSA_128S) { valid = (fw_pq_slh_dsa_128s_verify(s_pq_sig, sig_len, msg_bytes, msg_len, s_pq_pk) == 0); } if (valid) { char *res = build_alg_result_json(fw_alg_name(alg), NULL, "valid", "true"); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } } else { char *res = build_alg_result_json(fw_alg_name(alg), NULL, "valid", "false"); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } } } } } } free(msg_bytes); zeroize_pq_keys(); secure_memzero(req_privkey, sizeof(req_privkey)); return; } /* encapsulate / decapsulate (ml-kem-768) */ if (strcmp(method, VERB_ENCAPSULATE) == 0 || strcmp(method, VERB_DECAPSULATE) == 0) { cJSON *options = NULL; fw_alg_t alg; uint32_t index = 0; const char *peer_hex = NULL; const char *ct_hex = NULL; cJSON *arg0 = NULL; int is_decaps = (strcmp(method, VERB_DECAPSULATE) == 0); uint8_t peer_pk[FW_ML_KEM_768_PUBKEY_LEN]; uint8_t ct[FW_ML_KEM_768_CIPHERTEXT_LEN]; uint8_t ss[FW_ML_KEM_768_SHARED_SECRET_LEN]; if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } arg0 = cJSON_GetArrayItem(params, 0); if (!cJSON_IsString(arg0) || arg0->valuestring == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } parse_options_from_params(params, &options); if (parse_algorithm_from_options(options, &alg, &index) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (enforce_alg_verb(alg, method) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"algorithm_not_supported_for_verb\"}}", id_token, ERR_ALG_NOT_SUPPORTED); return; } { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, method); if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); return; } ui_add_event(method, caller_hex, "ml-kem-768"); if (!is_decaps) { /* encapsulate: arg0 = peer pubkey hex */ peer_hex = arg0->valuestring; if (hex_to_bytes(peer_hex, peer_pk, sizeof(peer_pk)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (fw_pq_ml_kem_768_encaps(ct, ss, peer_pk) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"encapsulate failed\"}}", id_token, ERR_INTERNAL); return; } { char ct_hex_buf[FW_ML_KEM_768_CIPHERTEXT_LEN * 2 + 1]; char ss_hex_buf[FW_ML_KEM_768_SHARED_SECRET_LEN * 2 + 1]; cJSON *obj = cJSON_CreateObject(); char *out = NULL; bytes_to_hex(ct, sizeof(ct), ct_hex_buf, sizeof(ct_hex_buf)); bytes_to_hex(ss, sizeof(ss), ss_hex_buf, sizeof(ss_hex_buf)); cJSON_AddStringToObject(obj, "ciphertext", ct_hex_buf); cJSON_AddStringToObject(obj, "shared_secret", ss_hex_buf); cJSON_AddStringToObject(obj, "algorithm", "ml-kem-768"); out = cJSON_PrintUnformatted(obj); if (out) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, out); cJSON_free(out); } cJSON_Delete(obj); } } else { /* decapsulate: arg0 = ciphertext hex; derive our keypair */ ct_hex = arg0->valuestring; if (hex_to_bytes(ct_hex, ct, sizeof(ct)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (derive_alg_key(alg, index, NULL, s_pq_hex, sizeof(s_pq_hex)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"key derivation failed\"}}", id_token, ERR_INTERNAL); return; } if (fw_pq_ml_kem_768_decaps(ss, ct, s_pq_sk) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"decapsulate failed\"}}", id_token, ERR_INTERNAL); } else { char ss_hex_buf[FW_ML_KEM_768_SHARED_SECRET_LEN * 2 + 1]; cJSON *obj = cJSON_CreateObject(); char *out = NULL; bytes_to_hex(ss, sizeof(ss), ss_hex_buf, sizeof(ss_hex_buf)); cJSON_AddStringToObject(obj, "shared_secret", ss_hex_buf); cJSON_AddStringToObject(obj, "algorithm", "ml-kem-768"); out = cJSON_PrintUnformatted(obj); if (out) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, out); cJSON_free(out); } cJSON_Delete(obj); } } } zeroize_pq_keys(); secure_memzero(peer_pk, sizeof(peer_pk)); secure_memzero(ct, sizeof(ct)); secure_memzero(ss, sizeof(ss)); return; } /* derive_shared_secret (x25519) */ if (strcmp(method, VERB_DERIVE_SHARED) == 0) { cJSON *options = NULL; fw_alg_t alg; uint32_t index = 0; const char *peer_hex = NULL; cJSON *arg0 = NULL; uint8_t req_privkey[32]; uint8_t peer_pub[32]; uint8_t shared[32]; memset(req_privkey, 0, sizeof(req_privkey)); memset(peer_pub, 0, sizeof(peer_pub)); memset(shared, 0, sizeof(shared)); if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } arg0 = cJSON_GetArrayItem(params, 0); if (!cJSON_IsString(arg0) || arg0->valuestring == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } peer_hex = arg0->valuestring; parse_options_from_params(params, &options); if (parse_algorithm_from_options(options, &alg, &index) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (enforce_alg_verb(alg, method) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"algorithm_not_supported_for_verb\"}}", id_token, ERR_ALG_NOT_SUPPORTED); return; } if (hex_to_bytes(peer_hex, peer_pub, 32) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, method); if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); secure_memzero(req_privkey, sizeof(req_privkey)); secure_memzero(peer_pub, sizeof(peer_pub)); return; } ui_add_event(method, caller_hex, "x25519"); if (derive_alg_key(alg, index, req_privkey, s_pq_hex, sizeof(s_pq_hex)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"key derivation failed\"}}", id_token, ERR_INTERNAL); } else if (x25519_ecdh(req_privkey, peer_pub, shared) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"ecdh failed\"}}", id_token, ERR_INTERNAL); } else { char ss_hex[65]; cJSON *obj = cJSON_CreateObject(); char *out = NULL; bytes_to_hex(shared, 32, ss_hex, sizeof(ss_hex)); cJSON_AddStringToObject(obj, "shared_secret", ss_hex); cJSON_AddStringToObject(obj, "algorithm", "x25519"); out = cJSON_PrintUnformatted(obj); if (out) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, out); cJSON_free(out); } cJSON_Delete(obj); } } secure_memzero(req_privkey, sizeof(req_privkey)); secure_memzero(peer_pub, sizeof(peer_pub)); secure_memzero(shared, sizeof(shared)); return; } /* derive (secp256k1 HMAC-SHA256) */ if (strcmp(method, VERB_DERIVE) == 0) { cJSON *options = NULL; fw_alg_t alg; uint32_t index = 0; const char *data_str = NULL; cJSON *arg0 = NULL; uint8_t req_privkey[32]; uint8_t mac[32]; char mac_hex[65]; cJSON *idx_item = NULL; memset(req_privkey, 0, sizeof(req_privkey)); memset(mac, 0, sizeof(mac)); if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } arg0 = cJSON_GetArrayItem(params, 0); if (!cJSON_IsString(arg0) || arg0->valuestring == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } data_str = arg0->valuestring; parse_options_from_params(params, &options); if (parse_algorithm_from_options(options, &alg, &index) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (enforce_alg_verb(alg, method) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"algorithm_not_supported_for_verb\"}}", id_token, ERR_ALG_NOT_SUPPORTED); return; } /* index is required for derive (no default) */ if (options == NULL || (idx_item = cJSON_GetObjectItemCaseSensitive(options, "index")) == NULL || !cJSON_IsNumber(idx_item)) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"missing_index\"}}", id_token, ERR_INVALID_PARAMS); return; } { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, method); if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); secure_memzero(req_privkey, sizeof(req_privkey)); return; } ui_add_event(method, caller_hex, "derive"); if (derive_alg_key(alg, index, req_privkey, s_pq_hex, sizeof(s_pq_hex)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"key derivation failed\"}}", id_token, ERR_INTERNAL); } else if (nostr_hmac_sha256(req_privkey, 32, (const unsigned char *)data_str, strlen(data_str), mac) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"hmac failed\"}}", id_token, ERR_INTERNAL); } else { derive_key_id_from_hex_pubkey(s_pq_hex, key_id); bytes_to_hex(mac, 32, mac_hex, sizeof(mac_hex)); char *res = build_alg_result_json("secp256k1", key_id, "digest", mac_hex); if (res) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, res); cJSON_free(res); } } } secure_memzero(req_privkey, sizeof(req_privkey)); secure_memzero(mac, sizeof(mac)); return; } /* encrypt / decrypt (otp) */ if (strcmp(method, VERB_ENCRYPT) == 0 || strcmp(method, VERB_DECRYPT) == 0) { cJSON *options = NULL; fw_alg_t alg; uint32_t index = 0; const char *payload = NULL; cJSON *arg0 = NULL; /* OTP encrypt and decrypt are the same XOR operation against the pad; * the verb name is carried through for the UI event only. */ (void)method; const char *encoding = "ascii"; /* parsed for API compatibility */ (void)encoding; if (!cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } arg0 = cJSON_GetArrayItem(params, 0); if (!cJSON_IsString(arg0) || arg0->valuestring == NULL) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } payload = arg0->valuestring; parse_options_from_params(params, &options); if (parse_algorithm_from_options(options, &alg, &index) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid params\"}}", id_token, ERR_INVALID_PARAMS); return; } if (enforce_alg_verb(alg, method) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"algorithm_not_supported_for_verb\"}}", id_token, ERR_ALG_NOT_SUPPORTED); return; } if (options != NULL) { cJSON *e = cJSON_GetObjectItemCaseSensitive(options, "encoding"); if (cJSON_IsString(e) && e->valuestring != NULL) { encoding = e->valuestring; } } if (s_otp_pad_len == 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"otp pad not initialized\"}}", id_token, ERR_INTERNAL); return; } { approval_decision_t d = prompt_non_sign_approval(caller_hex, 27235, method); if (d != APPROVAL_ONCE && d != APPROVAL_ALWAYS) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, (d == APPROVAL_DENY) ? ERR_DENIED_BY_USER : ERR_APPROVAL_TIMEOUT, (d == APPROVAL_DENY) ? "denied by user" : "approval timeout"); return; } ui_add_event(method, caller_hex, "otp"); /* For encrypt: payload is base64 plaintext (ascii) or base64 (binary). * For decrypt: payload is the ciphertext (base64 of the XOR output). * We decode base64, XOR with the pad at the current offset, advance * the offset, and re-encode. */ { uint8_t buf[1024]; size_t buf_len = 0; size_t need = 0; size_t i; if (b64_decode(payload, strlen(payload), buf, sizeof(buf), &buf_len) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"invalid base64\"}}", id_token, ERR_INVALID_PARAMS); return; } need = buf_len; if (s_otp_offset + need > s_otp_pad_len) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"otp pad exhausted\"}}", id_token, ERR_INTERNAL); return; } for (i = 0; i < buf_len; ++i) { buf[i] ^= s_otp_pad[s_otp_offset + i]; } s_otp_offset += buf_len; { char out_b64[1400]; if (b64_encode(buf, buf_len, out_b64, sizeof(out_b64)) != 0) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"internal error\"}}", id_token, ERR_INTERNAL); } else { cJSON *obj = cJSON_CreateObject(); char *out = NULL; cJSON_AddStringToObject(obj, "result", out_b64); cJSON_AddStringToObject(obj, "algorithm", "otp"); { char off_str[24]; snprintf(off_str, sizeof(off_str), "%u", (unsigned)s_otp_offset); cJSON_AddStringToObject(obj, "pad_offset", off_str); } out = cJSON_PrintUnformatted(obj); if (out) { snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}", id_token, out); cJSON_free(out); } cJSON_Delete(obj); } } secure_memzero(buf, sizeof(buf)); } } return; } /* Unknown verb */ snprintf(s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"method not found\"}}", id_token, ERR_METHOD_NOT_FOUND); } static void run_signing_loop(void) { uint32_t rx_frames = 0; uint32_t tx_frames = 0; size_t last_len = 0; while (1) { char stat1[32]; char stat2[32]; int got_frame = 0; int via_webusb = 0; if (webusb_recv_frame(s_frame_buf, sizeof(s_frame_buf), &last_len) == 0) { got_frame = 1; via_webusb = 1; } else if (recv_frame_stdin(s_frame_buf, sizeof(s_frame_buf), &last_len) == 0) { got_frame = 1; via_webusb = 0; } if (!got_frame) { ui_pump(); vTaskDelay(pdMS_TO_TICKS(2)); continue; } rx_frames++; /* Default to a concrete error so every received frame gets a response. */ snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32603,\"message\":\"internal error: unhandled request\"}}"); if (last_len < sizeof(s_frame_buf)) { char id_token[96] = {0}; char method[64] = {0}; char caller_pubkey_hex[65] = {0}; int auth_rc = 0; cJSON *req = NULL; s_frame_buf[last_len] = '\0'; if (extract_json_id_token((const char *)s_frame_buf, id_token, sizeof(id_token)) != 0) { strncpy(id_token, "null", sizeof(id_token) - 1); } req = cJSON_Parse((const char *)s_frame_buf); if (req == NULL || extract_json_string_field((const char *)s_frame_buf, "method", method, sizeof(method)) != 0) { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32600,\"message\":\"invalid request\"}}", id_token); } else { #if AUTH_REQUIRED auth_rc = auth_envelope_verify(req, method, caller_pubkey_hex); #else auth_rc = 0; strncpy(caller_pubkey_hex, "anon", sizeof(caller_pubkey_hex) - 1); #endif if (auth_rc != 0) { const char *msg = "auth envelope required"; if (auth_rc == AUTH_ERR_ENVELOPE_MALFORMED) msg = "auth envelope malformed"; else if (auth_rc == AUTH_ERR_BODY_MISMATCH) msg = "auth body mismatch"; else if (auth_rc == AUTH_ERR_SIGNATURE_INVALID) msg = "auth signature invalid"; else if (auth_rc == AUTH_ERR_KIND_INVALID) msg = "auth kind invalid"; else if (auth_rc == AUTH_ERR_REPLAY_DETECTED) msg = "auth replay detected"; snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":%d,\"message\":\"%s\"}}", id_token, auth_rc, msg); } else { /* Algorithm-based + Nostr verb dispatch (see handle_request). */ handle_request(req, method, id_token, caller_pubkey_hex); } } if (req != NULL) { cJSON_Delete(req); } if ((!via_webusb && send_frame_stdout((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0) || (via_webusb && webusb_send_frame((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0)) { tx_frames++; } else { ESP_LOGE(TAG, "failed to send JSON-RPC response over %s", via_webusb ? "WebUSB" : "CDC"); } } else { snprintf( s_response_buf, sizeof(s_response_buf), "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"request too large\"}}"); if ((!via_webusb && send_frame_stdout((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0) || (via_webusb && webusb_send_frame((const uint8_t *)s_response_buf, strlen(s_response_buf)) == 0)) { tx_frames++; } else { ESP_LOGE(TAG, "failed to send oversized-request error over %s", via_webusb ? "WebUSB" : "CDC"); } } (void)stat1; (void)stat2; (void)rx_frames; (void)tx_frames; (void)last_len; /* Keep LVGL alive so approval/idle screens render after the menu exits. */ ui_pump(); } } void app_main(void) { ESP_LOGI(TAG, "n_signer v%s booting...", FIRMWARE_VERSION); printf("n_signer booting\n"); ESP_ERROR_CHECK(display_init()); ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); memset(s_mnemonic, 0, sizeof(s_mnemonic)); memset(s_seed, 0, sizeof(s_seed)); memset(s_privkey, 0, sizeof(s_privkey)); memset(s_pubkey, 0, sizeof(s_pubkey)); memset(s_npub, 0, sizeof(s_npub)); memset(s_frame_buf, 0, sizeof(s_frame_buf)); memset(s_pubkey_hex, 0, sizeof(s_pubkey_hex)); #if CYD_DEBUG_AUTO_GENERATE if (ui_init_runtime() != 0) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "UI INIT FAIL", RGB565_RED, RGB565_BLACK)); return; } if (generate_mnemonic_12(s_mnemonic, sizeof(s_mnemonic)) != 0) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "MN GEN FAIL", RGB565_RED, RGB565_BLACK)); return; } #else if (ui_run_until_working(s_mnemonic) != 0) { ESP_ERROR_CHECK(display_clear(RGB565_BLACK)); ESP_ERROR_CHECK(display_draw_text_centered(56, "UI FAILED", RGB565_RED, RGB565_BLACK)); return; } #endif if (apply_mnemonic_and_enter_working(s_mnemonic) != 0) { secure_memzero(s_mnemonic, sizeof(s_mnemonic)); return; } secure_memzero(s_mnemonic, sizeof(s_mnemonic)); printf("usb cdc+webusb ready\n"); /* Prevent log text from corrupting framed transport bytes on this same USB stream. */ esp_log_level_set("*", ESP_LOG_NONE); buttons_init(); if (transport_init() != 0) { ESP_ERROR_CHECK(display_draw_text(10, 90, "cdc init fail", RGB565_RED, RGB565_BLACK)); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } } if (nostr_init() != NOSTR_SUCCESS) { ESP_ERROR_CHECK(display_draw_text(10, 90, "nostr init fail", RGB565_RED, RGB565_BLACK)); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } } /* WebUSB temporarily disabled to preserve stable USB Serial/JTAG CDC enumeration. */ run_signing_loop(); }