1006 lines
31 KiB
C
1006 lines
31 KiB
C
#include <inttypes.h>
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "driver/gpio.h"
|
|
|
|
#include "mbedtls/sha256.h"
|
|
#include "cJSON.h"
|
|
#include "secp256k1.h"
|
|
#include "secp256k1_extrakeys.h"
|
|
#include "secp256k1_schnorrsig.h"
|
|
|
|
#include "bech32.h"
|
|
#include "display.h"
|
|
#include "key_derivation.h"
|
|
#include "mnemonic.h"
|
|
#include "usb_transport.h"
|
|
|
|
#define FIRMWARE_VERSION "0.0.1"
|
|
#ifndef GIT_HASH
|
|
#define GIT_HASH "dev"
|
|
#endif
|
|
|
|
static const char *TAG = "nsigner";
|
|
|
|
#define BTN_DENY_GPIO 0 /* D0/BOOT, active LOW */
|
|
#define BTN_APPROVE_GPIO 1 /* D1, active HIGH */
|
|
#define BTN_ALWAYS_GPIO 2 /* D2, active HIGH */
|
|
|
|
#define APPROVAL_TIMEOUT_MS 30000
|
|
#define APPROVAL_DEBOUNCE_MS 30
|
|
|
|
typedef enum {
|
|
APPROVAL_DENY = 0,
|
|
APPROVAL_ONCE = 1,
|
|
APPROVAL_ALWAYS = 2,
|
|
APPROVAL_TIMEOUT = 3,
|
|
} approval_decision_t;
|
|
|
|
static bool s_buttons_inited = false;
|
|
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-S3 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 char s_npub_short[40];
|
|
static char s_mnemonic_short[64];
|
|
static uint8_t s_frame_buf[4096];
|
|
static char s_pubkey_hex[65];
|
|
static char s_response_buf[2048];
|
|
static char s_signed_event_buf[1600];
|
|
|
|
static int transport_init(void)
|
|
{
|
|
return usb_transport_init();
|
|
}
|
|
|
|
static void buttons_init(void)
|
|
{
|
|
if (s_buttons_inited) {
|
|
return;
|
|
}
|
|
|
|
gpio_config_t deny_cfg = {
|
|
.pin_bit_mask = 1ULL << BTN_DENY_GPIO,
|
|
.mode = GPIO_MODE_INPUT,
|
|
.pull_up_en = GPIO_PULLUP_ENABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
gpio_config(&deny_cfg);
|
|
|
|
gpio_config_t approve_cfg = {
|
|
.pin_bit_mask = (1ULL << BTN_APPROVE_GPIO) | (1ULL << BTN_ALWAYS_GPIO),
|
|
.mode = GPIO_MODE_INPUT,
|
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_ENABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
gpio_config(&approve_cfg);
|
|
|
|
s_buttons_inited = true;
|
|
}
|
|
|
|
static bool button_pressed_deny(void)
|
|
{
|
|
return gpio_get_level(BTN_DENY_GPIO) == 0;
|
|
}
|
|
|
|
static bool button_pressed_approve(void)
|
|
{
|
|
return gpio_get_level(BTN_APPROVE_GPIO) == 1;
|
|
}
|
|
|
|
static bool button_pressed_always(void)
|
|
{
|
|
return gpio_get_level(BTN_ALWAYS_GPIO) == 1;
|
|
}
|
|
|
|
static approval_decision_t wait_for_approval(uint32_t timeout_ms)
|
|
{
|
|
uint32_t elapsed = 0;
|
|
const uint32_t step = 20;
|
|
|
|
/* Drain any currently-held button before sampling fresh presses. */
|
|
while (button_pressed_deny() || button_pressed_approve() || button_pressed_always()) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
elapsed += 10;
|
|
if (elapsed >= timeout_ms) {
|
|
return APPROVAL_TIMEOUT;
|
|
}
|
|
}
|
|
|
|
while (elapsed < timeout_ms) {
|
|
if (button_pressed_deny()) {
|
|
vTaskDelay(pdMS_TO_TICKS(APPROVAL_DEBOUNCE_MS));
|
|
if (button_pressed_deny()) {
|
|
return APPROVAL_DENY;
|
|
}
|
|
}
|
|
if (button_pressed_always()) {
|
|
vTaskDelay(pdMS_TO_TICKS(APPROVAL_DEBOUNCE_MS));
|
|
if (button_pressed_always()) {
|
|
return APPROVAL_ALWAYS;
|
|
}
|
|
}
|
|
if (button_pressed_approve()) {
|
|
vTaskDelay(pdMS_TO_TICKS(APPROVAL_DEBOUNCE_MS));
|
|
if (button_pressed_approve()) {
|
|
return APPROVAL_ONCE;
|
|
}
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(step));
|
|
elapsed += step;
|
|
}
|
|
|
|
return APPROVAL_TIMEOUT;
|
|
}
|
|
|
|
static void show_approval_prompt_with_caller(const cJSON *event_in, const char *caller_pubkey_hex)
|
|
{
|
|
char line_caller[32];
|
|
char line_kind[32];
|
|
char line_content[64];
|
|
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;
|
|
}
|
|
|
|
if (caller_pubkey_hex != NULL && strlen(caller_pubkey_hex) >= 12) {
|
|
snprintf(line_caller, sizeof(line_caller), "from:%.6s..%.4s",
|
|
caller_pubkey_hex, caller_pubkey_hex + strlen(caller_pubkey_hex) - 4);
|
|
} else {
|
|
snprintf(line_caller, sizeof(line_caller), "from:%.20s",
|
|
caller_pubkey_hex ? caller_pubkey_hex : "anon");
|
|
}
|
|
|
|
snprintf(line_kind, sizeof(line_kind), "kind: %d", kind);
|
|
snprintf(line_content, sizeof(line_content), "%.50s", content);
|
|
|
|
display_clear(RGB565_BLACK);
|
|
display_draw_text(10, 10, "APPROVE sign_event?", RGB565_YELLOW, RGB565_BLACK);
|
|
display_draw_text(10, 24, line_caller, RGB565_MAGENTA, RGB565_BLACK);
|
|
display_draw_text(10, 40, line_kind, RGB565_WHITE, RGB565_BLACK);
|
|
display_draw_text(10, 56, line_content, RGB565_CYAN, RGB565_BLACK);
|
|
display_draw_text(10, 80, "D1=allow once", RGB565_GREEN, RGB565_BLACK);
|
|
display_draw_text(10, 96, "D2=always allow", RGB565_GREEN, RGB565_BLACK);
|
|
display_draw_text(10, 112, "D0=deny", RGB565_RED, RGB565_BLACK);
|
|
}
|
|
|
|
static void show_idle_screen(void)
|
|
{
|
|
display_clear(RGB565_BLACK);
|
|
display_draw_text(10, 10, "nostr npub", RGB565_GREEN, RGB565_BLACK);
|
|
display_draw_text(10, 30, s_npub_short, RGB565_GREEN, RGB565_BLACK);
|
|
display_draw_text(10, 50, "mn:", RGB565_WHITE, RGB565_BLACK);
|
|
display_draw_text(46, 50, s_mnemonic_short, RGB565_YELLOW, RGB565_BLACK);
|
|
display_draw_text(10, 70, "v" FIRMWARE_VERSION, RGB565_WHITE, RGB565_BLACK);
|
|
display_draw_text(10, 90, "ready", RGB565_CYAN, RGB565_BLACK);
|
|
}
|
|
|
|
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 format_npub_short(const char *npub, char *out, size_t out_len)
|
|
{
|
|
size_t len;
|
|
|
|
if (out == NULL || out_len == 0) {
|
|
return;
|
|
}
|
|
|
|
if (npub == NULL) {
|
|
out[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
len = strlen(npub);
|
|
if (len <= 22) {
|
|
memcpy(out, npub, len + 1);
|
|
return;
|
|
}
|
|
|
|
snprintf(out, out_len, "%.11s...%s", npub, npub + len - 6);
|
|
}
|
|
|
|
static void format_mnemonic_short(const char *mnemonic, char *out, size_t out_len)
|
|
{
|
|
size_t copied = 0;
|
|
int spaces_seen = 0;
|
|
|
|
if (out == NULL || out_len == 0) {
|
|
return;
|
|
}
|
|
|
|
out[0] = '\0';
|
|
|
|
if (mnemonic == NULL) {
|
|
return;
|
|
}
|
|
|
|
while (*mnemonic != '\0' && copied + 1 < out_len) {
|
|
out[copied++] = *mnemonic;
|
|
if (*mnemonic == ' ') {
|
|
spaces_seen++;
|
|
if (spaces_seen >= 3) {
|
|
break;
|
|
}
|
|
}
|
|
mnemonic++;
|
|
}
|
|
|
|
if (copied >= 4 && copied + 3 < out_len) {
|
|
out[copied++] = '.';
|
|
out[copied++] = '.';
|
|
out[copied++] = '.';
|
|
}
|
|
|
|
out[copied] = '\0';
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void app_main(void)
|
|
{
|
|
uint32_t rx_frames = 0;
|
|
uint32_t tx_frames = 0;
|
|
size_t last_len = 0;
|
|
|
|
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));
|
|
|
|
ESP_ERROR_CHECK(display_draw_text(10, 10, "n_signer", RGB565_YELLOW, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 30, "deriving keys...", RGB565_YELLOW, 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_npub_short, 0, sizeof(s_npub_short));
|
|
memset(s_mnemonic_short, 0, sizeof(s_mnemonic_short));
|
|
memset(s_frame_buf, 0, sizeof(s_frame_buf));
|
|
memset(s_pubkey_hex, 0, sizeof(s_pubkey_hex));
|
|
|
|
if (generate_mnemonic_12(s_mnemonic, sizeof(s_mnemonic)) != 0) {
|
|
ESP_ERROR_CHECK(display_draw_text(10, 50, "mnemonic failed", RGB565_RED, RGB565_BLACK));
|
|
return;
|
|
}
|
|
|
|
printf("BIP39 mnemonic: %s\n", s_mnemonic);
|
|
format_mnemonic_short(s_mnemonic, s_mnemonic_short, sizeof(s_mnemonic_short));
|
|
|
|
if (mnemonic_to_seed(s_mnemonic, s_seed) != 0) {
|
|
ESP_ERROR_CHECK(display_draw_text(10, 50, "seed failed", RGB565_RED, RGB565_BLACK));
|
|
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
|
return;
|
|
}
|
|
|
|
if (derive_nostr_key(s_seed, s_privkey, s_pubkey) != 0) {
|
|
ESP_ERROR_CHECK(display_draw_text(10, 50, "key failed", RGB565_RED, RGB565_BLACK));
|
|
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
|
memset(s_seed, 0, sizeof(s_seed));
|
|
return;
|
|
}
|
|
|
|
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));
|
|
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));
|
|
return;
|
|
}
|
|
|
|
format_npub_short(s_npub, s_npub_short, sizeof(s_npub_short));
|
|
bytes_to_hex(s_pubkey, sizeof(s_pubkey), s_pubkey_hex, sizeof(s_pubkey_hex));
|
|
printf("Derived npub: %s\n", s_npub);
|
|
printf("Derived pubkey hex: %s\n", s_pubkey_hex);
|
|
|
|
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 10, "nostr npub", RGB565_GREEN, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 30, s_npub_short, RGB565_GREEN, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 50, "mn:", RGB565_WHITE, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(46, 50, s_mnemonic_short, RGB565_YELLOW, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 70, "v" FIRMWARE_VERSION, RGB565_WHITE, RGB565_BLACK));
|
|
|
|
memset(s_seed, 0, sizeof(s_seed));
|
|
memset(s_pubkey, 0, sizeof(s_pubkey));
|
|
memset(s_mnemonic, 0, sizeof(s_mnemonic));
|
|
|
|
ESP_ERROR_CHECK(display_draw_text(10, 90, "usb cdc+webusb", RGB565_CYAN, RGB565_BLACK));
|
|
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));
|
|
}
|
|
}
|
|
|
|
/* WebUSB temporarily disabled to preserve stable USB Serial/JTAG CDC enumeration. */
|
|
|
|
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) {
|
|
vTaskDelay(pdMS_TO_TICKS(2));
|
|
continue;
|
|
}
|
|
|
|
rx_frames++;
|
|
|
|
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 if (strcmp(method, "get_public_key") == 0) {
|
|
snprintf(
|
|
s_response_buf,
|
|
sizeof(s_response_buf),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":\"%s\"}",
|
|
id_token,
|
|
s_pubkey_hex);
|
|
} else if (strcmp(method, "sign_event") == 0) {
|
|
cJSON *params = cJSON_GetObjectItemCaseSensitive(req, "params");
|
|
cJSON *event_in = NULL;
|
|
|
|
if (cJSON_IsArray(params) && cJSON_GetArraySize(params) > 0) {
|
|
event_in = cJSON_GetArrayItem(params, 0);
|
|
}
|
|
|
|
if (event_in != NULL && cJSON_IsObject(event_in)) {
|
|
approval_decision_t decision;
|
|
|
|
if (s_always_allow_sign) {
|
|
decision = APPROVAL_ALWAYS;
|
|
} else {
|
|
show_approval_prompt_with_caller(event_in, caller_pubkey_hex);
|
|
decision = wait_for_approval(APPROVAL_TIMEOUT_MS);
|
|
show_idle_screen();
|
|
}
|
|
|
|
if (decision == APPROVAL_ALWAYS) {
|
|
s_always_allow_sign = true;
|
|
}
|
|
|
|
if ((decision == APPROVAL_ONCE || decision == APPROVAL_ALWAYS) &&
|
|
build_signed_event_json(event_in, s_pubkey_hex, s_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);
|
|
} else if (decision == APPROVAL_DENY) {
|
|
snprintf(
|
|
s_response_buf,
|
|
sizeof(s_response_buf),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32000,\"message\":\"denied by user\"}}",
|
|
id_token);
|
|
} else if (decision == APPROVAL_TIMEOUT) {
|
|
snprintf(
|
|
s_response_buf,
|
|
sizeof(s_response_buf),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32001,\"message\":\"approval timeout\"}}",
|
|
id_token);
|
|
} else {
|
|
snprintf(
|
|
s_response_buf,
|
|
sizeof(s_response_buf),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32603,\"message\":\"internal error\"}}",
|
|
id_token);
|
|
}
|
|
} else {
|
|
snprintf(
|
|
s_response_buf,
|
|
sizeof(s_response_buf),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32602,\"message\":\"invalid params\"}}",
|
|
id_token);
|
|
}
|
|
} else {
|
|
snprintf(
|
|
s_response_buf,
|
|
sizeof(s_response_buf),
|
|
"{\"jsonrpc\":\"2.0\",\"id\":%s,\"error\":{\"code\":-32601,\"message\":\"method not found\"}}",
|
|
id_token);
|
|
}
|
|
}
|
|
|
|
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++;
|
|
}
|
|
}
|
|
|
|
snprintf(stat1, sizeof(stat1), "rx:%" PRIu32 " tx:%" PRIu32, rx_frames, tx_frames);
|
|
snprintf(stat2, sizeof(stat2), "last:%uB", (unsigned)last_len);
|
|
ESP_ERROR_CHECK(display_fill_rect(10, 110, 220, 24, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 110, stat1, RGB565_WHITE, RGB565_BLACK));
|
|
ESP_ERROR_CHECK(display_draw_text(10, 122, stat2, RGB565_CYAN, RGB565_BLACK));
|
|
}
|
|
}
|