Files

2574 lines
100 KiB
C

#include <inttypes.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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":"<alg>","key_id":"<16hex>","<field>":"<value>"}
* 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", "<n>", "<difficulty>"] */
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();
}