Files
n_signer/firmware/teensy41/signer/signer.ino
T

472 lines
16 KiB
Arduino

// Teensy 4.1 n_signer — main firmware entry.
//
// Phase 7 (final) of plans/teensy41_signer_implementation.md.
//
// This is the real, integrated signer firmware. It boots, shows a startup
// menu (generate or enter mnemonic), derives the secp256k1 keypair, encodes
// the npub, zeroizes the mnemonic text, initializes the dispatch + USB CDC
// transport layers, shows the idle screen, and enters the signing loop where
// it pumps LVGL and polls transport_read_frame() for incoming JSON-RPC
// requests.
//
// Signer state (g_seed / g_privkey / g_pubkey / g_npub / g_pubkey_hex /
// g_signer_ready / g_seed_len) is *defined* in src/dispatch.cpp (declared
// extern in src/dispatch.h) so dispatch.cpp can read them directly. This
// file only populates them after the startup menu applies the mnemonic.
//
// See plans/teensy41_signer_implementation.md for the full plan.
// Debug aid: skip the startup menu and auto-generate a fresh random mnemonic
// each boot. Mirrors CYD_DEBUG_AUTO_GENERATE in
// firmware/cyd_esp32_2432s028/main/main.c. Set to 0 for the normal interactive
// boot flow (show the startup menu, wait for the user to tap Generate/Enter).
#define DEBUG_AUTO_GENERATE 1
#include <lvgl.h>
#include <ST7796_t3.h>
#include <SPI.h>
// ---- Component headers (Arduino auto-discovers the .cpp files in src/,
// but the .h files must be explicitly included here). ----
#include "src/mnemonic.h"
#include "src/key_derivation.h"
#include "src/bech32.h"
#include "src/transport.h"
#include "src/dispatch.h"
#include "src/ui.h"
#include "src/secure_mem.h"
#include "src/otp_pad.h"
#include "src/nostr_core/nostr_common.h"
// ---- Pin map (from firmware/teensy41/WIRING.md) ----
#define TFT_CS 5
#define TFT_RST 6
#define TFT_DC 7
#define TFT_BL 8
#define T_CS 9
#define T_IRQ 2 // NOT pin 10 (SPI0 CS0 conflict)
#define SPI_MOSI 11
#define SPI_MISO 12
#define SPI_SCK 13
// ---- Display constants ----
#define SCREEN_W 480
#define SCREEN_H 320
ST7796_t3 tft = ST7796_t3(TFT_CS, TFT_DC, TFT_RST);
// ---- LVGL draw buffer (in DMAMEM/RAM2 to save RAM1).
// Reduced from /10 to /20 (23 KB each instead of 46 KB) to fit the 512 KB
// RAM1 budget alongside the secp256k1 + PQClean code in ITCM. The flush
// callback uses drawPixel (slow), so partial-render mode handles this fine.
static const uint32_t LV_BUF_PIXELS = SCREEN_W * SCREEN_H / 20;
DMAMEM static lv_color_t lv_buf1[LV_BUF_PIXELS];
DMAMEM static lv_color_t lv_buf2[LV_BUF_PIXELS];
// ---- Aesthetics colors (from ~/lt/aesthetics/WEB.md) ----
#define UI_BG 0x000000 // black
#define UI_FG 0xFFFFFF // white
#define UI_ACCENT 0xFF0000 // red
#define UI_MUTED 0x888888 // grey
// ---- Touch calibration (from firmware/teensy41/README.md) ----
struct TouchCal { int x_min, x_max, y_min, y_max; bool invert_x, invert_y; };
static TouchCal s_cal = { 207, 1909, 168, 1798, true, true };
#define XPT_X 0xD0
#define XPT_Y 0x90
#define XPT_Z1 0xB0
#define XPT_Z2 0xC0
#define T_SPI_SPEED 2000000
static uint16_t xpt_read(uint8_t ctrl) {
SPI.beginTransaction(SPISettings(T_SPI_SPEED, MSBFIRST, SPI_MODE0));
digitalWrite(T_CS, LOW);
SPI.transfer(ctrl);
uint8_t hi = SPI.transfer(0x00);
uint8_t lo = SPI.transfer(0x00);
digitalWrite(T_CS, HIGH);
SPI.endTransaction();
return (((uint16_t)hi << 8) | lo) >> 4;
}
static bool touch_read_raw(uint16_t *x, uint16_t *y, uint16_t *z) {
if (digitalRead(T_IRQ) != 0) return false;
uint32_t sx=0, sy=0, sz=0; int valid=0;
for (int i=0; i<7; i++) {
uint16_t z1=xpt_read(XPT_Z1), z2=xpt_read(XPT_Z2);
uint16_t p = (z1>0 && z2>z1) ? (uint16_t)(z1+(4095-z2)) : 0;
if (p < 80) continue;
sx += xpt_read(XPT_X); sy += xpt_read(XPT_Y); sz += p; ++valid;
}
if (!valid) return false;
*x=sx/valid; *y=sy/valid; *z=sz/valid;
return true;
}
static int map_clamped(int v, int imn, int imx, int omn, int omx) {
if (v<imn) v=imn; if (v>imx) v=imx;
int d=imx-imn; if (!d) return omn;
return omn + (v-imn)*(omx-omn)/d;
}
static void raw_to_screen(uint16_t rx, uint16_t ry, int *sx, int *sy) {
*sx = map_clamped((int)ry, s_cal.x_min, s_cal.x_max,
s_cal.invert_x ? (SCREEN_W-1) : 0,
s_cal.invert_x ? 0 : (SCREEN_W-1));
*sy = map_clamped((int)rx, s_cal.y_min, s_cal.y_max,
s_cal.invert_y ? (SCREEN_H-1) : 0,
s_cal.invert_y ? 0 : (SCREEN_H-1));
}
// ---- LVGL callbacks ----
static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
uint16_t w = area->x2 - area->x1 + 1;
uint16_t h = area->y2 - area->y1 + 1;
uint16_t *px = (uint16_t *)px_map;
for (uint16_t y=0; y<h; y++)
for (uint16_t x=0; x<w; x++)
tft.drawPixel(area->x1+x, area->y1+y, px[y*w+x]);
lv_display_flush_ready(disp);
}
static void lvgl_touch_cb(lv_indev_t *indev, lv_indev_data_t *data) {
uint16_t rx, ry, rz;
if (touch_read_raw(&rx, &ry, &rz)) {
int sx, sy; raw_to_screen(rx, ry, &sx, &sy);
data->point.x = sx; data->point.y = sy;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
// ---- Aesthetics: style helpers ----
static void style_screen(lv_obj_t *scr) {
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(scr, lv_color_hex(UI_BG), 0);
}
static void style_button(lv_obj_t *btn) {
lv_obj_set_style_radius(btn, 6, 0);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(UI_BG), 0);
lv_obj_set_style_border_width(btn, 2, 0);
lv_obj_set_style_border_color(btn, lv_color_hex(UI_FG), 0);
lv_obj_set_style_text_color(btn, lv_color_hex(UI_FG), 0);
lv_obj_set_style_border_color(btn, lv_color_hex(UI_ACCENT), LV_STATE_PRESSED);
lv_obj_set_style_text_color(btn, lv_color_hex(UI_ACCENT), LV_STATE_PRESSED);
lv_obj_set_style_border_color(btn, lv_color_hex(UI_ACCENT), LV_STATE_FOCUSED);
lv_obj_set_style_text_color(btn, lv_color_hex(UI_ACCENT), LV_STATE_FOCUSED);
}
// ---- Startup menu state ----
typedef enum { MODE_NONE, MODE_GENERATE, MODE_ENTER } signer_mode_t;
static signer_mode_t s_mode = MODE_NONE;
static volatile bool s_mode_selected = false;
// Mnemonic working buffer (zeroized after seed derivation).
static char s_mnemonic[256];
// ---- Startup menu ----
static void on_generate(lv_event_t *e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
s_mode = MODE_GENERATE;
s_mode_selected = true;
Serial.println("Generate selected");
}
}
static void on_enter(lv_event_t *e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
s_mode = MODE_ENTER;
s_mode_selected = true;
Serial.println("Enter selected");
}
}
static void build_startup_menu() {
lv_obj_t *scr = lv_screen_active();
lv_obj_clean(scr);
style_screen(scr);
// Title
lv_obj_t *title = lv_label_create(scr);
lv_label_set_text(title, "n_signer");
lv_obj_set_style_text_color(title, lv_color_hex(UI_FG), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 30);
// Subtitle
lv_obj_t *sub = lv_label_create(scr);
lv_label_set_text(sub, "Hardware signer");
lv_obj_set_style_text_color(sub, lv_color_hex(UI_MUTED), 0);
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 60);
// Generate button
lv_obj_t *btn_gen = lv_button_create(scr);
style_button(btn_gen);
lv_obj_set_size(btn_gen, 360, 50);
lv_obj_align(btn_gen, LV_ALIGN_CENTER, 0, -40);
lv_obj_add_event_cb(btn_gen, on_generate, LV_EVENT_ALL, NULL);
lv_obj_t *lbl_gen = lv_label_create(btn_gen);
lv_label_set_text(lbl_gen, "Generate New Mnemonic");
lv_obj_center(lbl_gen);
// Enter button
lv_obj_t *btn_ent = lv_button_create(scr);
style_button(btn_ent);
lv_obj_set_size(btn_ent, 360, 50);
lv_obj_align(btn_ent, LV_ALIGN_CENTER, 0, 30);
lv_obj_add_event_cb(btn_ent, on_enter, LV_EVENT_ALL, NULL);
lv_obj_t *lbl_ent = lv_label_create(btn_ent);
lv_label_set_text(lbl_ent, "Enter Existing Mnemonic");
lv_obj_center(lbl_ent);
// Footer
lv_obj_t *footer = lv_label_create(scr);
lv_label_set_text(footer, "v0.1.0-teensy41");
lv_obj_set_style_text_color(footer, lv_color_hex(UI_MUTED), 0);
lv_obj_align(footer, LV_ALIGN_BOTTOM_MID, 0, -8);
}
// ---- "Busy" screen shown while deriving keys (so the user sees something
// during the PBKDF2 + secp256k1 work, which takes a moment). ----
static void build_busy_screen(const char *msg) {
lv_obj_t *scr = lv_screen_active();
lv_obj_clean(scr);
style_screen(scr);
lv_obj_t *lbl = lv_label_create(scr);
lv_label_set_text(lbl, msg);
lv_obj_set_style_text_color(lbl, lv_color_hex(UI_FG), 0);
lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0);
// Render at least one frame so the message is actually visible.
lv_tick_inc(5);
lv_timer_handler();
}
// ---- Hex helper (lowercase) for the 32-byte pubkey -> g_pubkey_hex. ----
static void bytes_to_hex(const uint8_t *in, size_t n, char *out) {
static const char hexdigits[] = "0123456789abcdef";
for (size_t i = 0; i < n; i++) {
out[i * 2] = hexdigits[(in[i] >> 4) & 0xF];
out[i * 2 + 1] = hexdigits[in[i] & 0xF];
}
out[n * 2] = '\0';
}
// ---- Apply the mnemonic: derive seed + secp256k1 keys + npub, zeroize the
// mnemonic text, and mark the signer ready. Returns 0 on success. ----
static int apply_mnemonic() {
Serial.println("Deriving seed...");
if (mnemonic_to_seed(s_mnemonic, g_seed) != 0) {
Serial.println("mnemonic_to_seed failed");
return -1;
}
g_seed_len = 64;
// Initialize the OTP pad from the seed so encrypt/decrypt verbs work.
Serial.println("Initializing OTP pad...");
if (otp_pad_init(g_seed, g_seed_len) != 0) {
Serial.println("otp_pad_init failed");
return -1;
}
Serial.println("Deriving secp256k1 keys...");
if (derive_secp256k1_keys(g_seed, g_seed_len, g_privkey, g_pubkey) != 0) {
Serial.println("derive_secp256k1_keys failed");
return -1;
}
Serial.println("Encoding npub...");
if (npub_encode(g_pubkey, g_npub, sizeof(g_npub)) != 0) {
Serial.println("npub_encode failed");
return -1;
}
bytes_to_hex(g_pubkey, 32, g_pubkey_hex);
// The mnemonic text is no longer needed — wipe it. The seed stays in RAM
// (zeroized on power-off) so we can derive per-algorithm keys on demand.
secure_memzero(s_mnemonic, sizeof(s_mnemonic));
g_signer_ready = 1;
Serial.print("Ready. npub: ");
Serial.println(g_npub);
return 0;
}
// ---- Boot flow: block on the startup menu, then run the generate/enter
// subscreen, then apply the mnemonic. Returns 0 on success. ----
//
// When DEBUG_AUTO_GENERATE is 1, the interactive startup menu is skipped: a
// random 12-word mnemonic is generated immediately, the mnemonic display is
// skipped, and the seed + keys are derived so the signer can enter the
// signing loop without any user interaction. This is intended for testing.
static int run_boot_flow() {
#if DEBUG_AUTO_GENERATE
Serial.println("DEBUG_AUTO_GENERATE=1: skipping startup menu.");
Serial.println("Generating 12-word mnemonic...");
if (generate_mnemonic_12(s_mnemonic, sizeof(s_mnemonic)) != 0) {
Serial.println("generate_mnemonic_12 failed");
return -1;
}
Serial.println("Mnemonic generated; skipping display (DEBUG_AUTO_GENERATE).");
// Skip ui_show_mnemonic — proceed straight to key derivation.
#else
// Pump LVGL until the user picks Generate or Enter.
while (!s_mode_selected) {
lv_tick_inc(5);
lv_timer_handler();
delay(5);
}
s_mode_selected = false;
if (s_mode == MODE_GENERATE) {
Serial.println("Generating 12-word mnemonic...");
if (generate_mnemonic_12(s_mnemonic, sizeof(s_mnemonic)) != 0) {
Serial.println("generate_mnemonic_12 failed");
return -1;
}
Serial.println("Mnemonic generated; showing to user.");
// ui_show_mnemonic blocks (pumps LVGL) until the user taps to continue.
ui_show_mnemonic(s_mnemonic);
} else {
Serial.println("Awaiting mnemonic entry on keyboard...");
// ui_enter_mnemonic blocks until 12 valid words + checksum, or cancel.
if (ui_enter_mnemonic(s_mnemonic, sizeof(s_mnemonic)) != 0) {
Serial.println("Mnemonic entry cancelled.");
return -1;
}
}
#endif
build_busy_screen("Deriving keys...");
if (apply_mnemonic() != 0) {
return -1;
}
return 0;
}
// ---- Crash diagnostics (debug) ----
// These run at the very start of setup(), before transport_init(), so Serial
// output here does NOT corrupt the framed transport stream. They print the
// CrashReport from any prior hard fault (fault type, PC, LR, SP) plus the
// last-action marker so we can diagnose the get_public_key crash.
void setup() {
Serial.begin(115200);
while (!Serial && millis() < 3000) ;
pinMode(LED_BUILTIN, OUTPUT);
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
// Print any pending crash report from a prior fault (survives soft reboot).
if (CrashReport) {
Serial.print("CRASH REPORT FROM PRIOR FAULT:");
Serial.println();
Serial.print(CrashReport);
Serial.println();
CrashReport.clear();
}
Serial.println("n_signer booting...");
tft.init(320, 480);
tft.setRotation(1);
tft.fillScreen(0x0000);
pinMode(T_CS, OUTPUT);
digitalWrite(T_CS, HIGH);
pinMode(T_IRQ, INPUT);
lv_init();
lv_display_t *disp = lv_display_create(SCREEN_W, SCREEN_H);
lv_display_set_buffers(disp, lv_buf1, lv_buf2,
LV_BUF_PIXELS * sizeof(lv_color_t),
LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(disp, lvgl_flush_cb);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, lvgl_touch_cb);
build_startup_menu();
Serial.println("Startup menu shown.");
// Run the boot flow (blocks until a mnemonic is loaded or entry cancelled).
if (run_boot_flow() != 0) {
// Entry cancelled or derivation failed — return to the menu and retry.
Serial.println("Boot flow failed; returning to startup menu.");
s_mode = MODE_NONE;
build_startup_menu();
// Re-run the boot flow. On a second cancel this recurses via loop();
// to keep it simple we just loop here until we get a valid mnemonic.
while (run_boot_flow() != 0) {
s_mode = MODE_NONE;
build_startup_menu();
}
}
// Initialize the nostr_core library (creates the secp256k1 context used by
// NIP-04/NIP-44 ECDH, event signing, and event verification).
if (nostr_init() != 0) {
Serial.println("nostr_init failed!");
} else {
Serial.println("nostr_core initialized.");
}
// Initialize the dispatch layer (nonce cache, etc).
dispatch_init();
Serial.println("Dispatch initialized.");
// Initialize the USB CDC transport.
transport_init();
Serial.println("Transport initialized.");
// Show the idle screen with the npub + version.
ui_show_idle(g_npub, "v0.1.0-teensy41");
Serial.println("Idle screen shown. Signer ready.");
}
// ---- Signing loop buffers (in DMAMEM/RAM2 to save RAM1) ----
DMAMEM static uint8_t req_buf[2048];
DMAMEM static char resp_buf[4096];
void loop() {
// 1. Keep LVGL responsive (idle screen + any approval prompts).
lv_tick_inc(5);
lv_timer_handler();
// 2. Non-blocking poll for an incoming JSON-RPC request frame.
int len = transport_read_frame(req_buf, sizeof(req_buf), 0);
if (len > 0) {
// NUL-terminate so dispatch can treat it as a string.
if ((size_t)len < sizeof(req_buf)) {
req_buf[len] = '\0';
} else {
req_buf[sizeof(req_buf) - 1] = '\0';
}
// 3. Dispatch the request -> JSON-RPC response.
// dispatch_handle_request may call ui_approve() for signing verbs,
// which runs its own LVGL loop until the user decides.
int resp_len = dispatch_handle_request((const char *)req_buf,
resp_buf, sizeof(resp_buf));
if (resp_len > 0) {
// 4. Send the framed response back over USB CDC.
// Do NOT use Serial.print here — it corrupts the framed transport
// stream (debug text gets mixed with framed response bytes).
transport_write_frame((const uint8_t *)resp_buf, resp_len);
transport_flush();
}
// Refresh the idle screen after handling a request (in case an approval
// prompt replaced it). ui_show_idle does not block.
ui_show_idle(g_npub, "v0.1.0-teensy41");
}
delay(5);
}