Files
n_signer/plans/teensy41_signer_implementation.md

16 KiB
Raw Permalink Blame History

Plan: Teensy 4.1 Signer Implementation

Remaining correctness and performance work is tracked in teensy41_signer_remaining_fixes.md.

Goal: turn the Teensy 4.1 + ST7796S display + XPT2046 touch into a working n_signer hardware signer — same verb set and wire protocol as the CYD firmware, same mnemonic-in-working-memory-only security model, with the aesthetics from ~/lt/aesthetics (black/white/red, monospaced, menu-first).

Phase 0 (bring-up) is complete — see teensy41_bringup.md. This plan covers Phases 1-7: crypto → keys → display/touch → UI → transport → dispatch → integration.

Design principles (from the user)

  1. Mnemonic is working-memory only. The seed phrase is never written to flash or SD. Power off → it's gone. The CYD already does this (secure_memzero(s_mnemonic) after mnemonic_to_seed); we keep that.
  2. Generate random OR enter your own. Startup menu offers both paths.
  3. Aesthetics (from ~/lt/aesthetics/WEB.md):
    • Black background (0x0000), white foreground (0xFFFF), red accent (0xF800) for active/pressed/selection/warnings only.
    • Muted grey (0x4208#444) for disabled/secondary text and borders.
    • Monospaced font (the ST7796_t3 library's built-in glcd font).
    • Menu-first interaction: every screen has an obvious next action.
    • Single-key/tap primary actions; fast cancellation (an "X" / back button).
    • Rounded corners (6px radius) on buttons, 2px white borders, no gradients/shadows.
    • State visibility: show npub, version, and connection status on idle.

Architecture

flowchart TD
    USB[Host USB CDC] --> Frame[transport.cpp<br/>4-byte length prefix + JSON-RPC]
    Frame --> Auth[auth envelope verify<br/>secp256k1 schnorr]
    Auth -->|ok| Disp[dispatch.cpp<br/>handle_request]
    Auth -->|fail| Err[auth error JSON]
    Disp -->|sign/get_pubkey/etc| KD[key_derivation.cpp<br/>secp256k1/ed25519/x25519/PQ]
    Disp -->|encrypt/decrypt| OTP[otp_pad.cpp<br/>HKDF-derived pad from seed]
    KD --> UI[ui.cpp<br/>approval prompt<br/>ST7796S + XPT2046]
    UI -->|approve| Exec[execute verb]
    UI -->|deny| Deny[deny JSON]
    Exec --> Resp[structured JSON result]
    Resp --> Frame
    Startup[Boot] --> Menu[startup menu<br/>generate or enter mnemonic]
    Menu --> Seed[mnemonic_to_seed<br/>PBKDF2-HMAC-SHA512]
    Seed --> Zero[secure_memzero mnemonic]
    Zero --> Idle[idle screen<br/>show npub]
    Idle --> Frame

File layout

firmware/teensy41/
├── README.md                  (updated — calibrated values, status)
├── WIRING.md                  (done — pin assignments)
├── teensy41_signer.ino        (Arduino entry — setup() + loop())
├── src/
│   ├── display.cpp            (ST7796S wrapper: init, fill, text, rect, blit)
│   ├── display.h
│   ├── touch.cpp              (XPT2046: raw read, calibrated screen coords)
│   ├── touch.h                (TouchCal struct + calibrated constants)
│   ├── ui.cpp                 (UI screens: startup, mnemonic, idle, approval)
│   ├── ui.h
│   ├── ui_primitives.cpp      (buttons, keyboard, text input, scrollable list)
│   ├── ui_primitives.h
│   ├── key_derivation.cpp     (BIP-39 → seed → secp256k1/ed25519/x25519/PQ keys)
│   ├── key_derivation.h
│   ├── mnemonic.cpp           (BIP-39 wordlist + generate + validate + to_seed)
│   ├── mnemonic.h
│   ├── mnemonic_wordlist.h    (the 2048 BIP-39 words — port from CYD)
│   ├── bech32.cpp             (npub encoding — port from CYD)
│   ├── bech32.h
│   ├── pq_crypto.cpp          (PQClean wrappers: ml-dsa-65, slh-dsa-128s, ml-kem-768)
│   ├── pq_crypto.h
│   ├── otp_pad.cpp            (HKDF-derived pad from seed — port from CYD)
│   ├── otp_pad.h
│   ├── transport.cpp          (USB CDC + 4-byte length-prefix framing)
│   ├── transport.h
│   ├── dispatch.cpp           (verb dispatch — port from CYD handle_request)
│   ├── dispatch.h
│   ├── auth_envelope.cpp      (secp256k1 schnorr auth verify — port from src/)
│   ├── auth_envelope.h
│   ├── secure_mem.cpp         (zeroize helpers)
│   ├── secure_mem.h
│   ├── cJSON.c                (vendored cJSON — port from CYD)
│   ├── cJSON.h
│   └── font5x7.h              (optional: custom font — or use ST7796_t3 built-in)
├── lib/
│   ├── secp256k1/             (libsecp256k1, built for ARM Cortex-M7)
│   ├── pqclean/               (PQClean ML-DSA-65, SLH-DSA-128s, ML-KEM-768)
│   └── nostr_core_lib/        (nip004, nip044, nostr_common, utils — portable C)
└── documents/                 (datasheets — already present)

Aesthetics: color constants

// RGB565 values matching ~/lt/aesthetics/WEB.md
#define UI_COLOR_BG       0x0000   // black — primary background
#define UI_COLOR_FG       0xFFFF   // white — primary foreground / text
#define UI_COLOR_ACCENT   0xF800   // red — active/pressed/selection/warning
#define UI_COLOR_MUTED    0x4208   // grey (~#444) — disabled/secondary/borders
#define UI_COLOR_DISABLED 0x2104   // darker grey (~#222) — disabled text

Phase 1: Crypto stack

Port the crypto libraries for ARM Cortex-M7 (600 MHz, no FPU issues, plenty of RAM). The Teensy has 1 MB SRAM + 16 MB PSRAM, so memory is not a constraint.

  1. secp256k1 — build libsecp256k1 for ARM Cortex-M7 with USE_NUM_NONE USE_FIELD_INV_BUILTIN USE_SCALAR_INV_BUILTIN (no ASM). Verify schnorr sign/verify + ECDSA against known test vectors. Source: the kb2040_hidden_signer already has a vendored secp256k1 at firmware/kb2040_hidden_signer/src/secp256k1/ — reuse that.
  2. SHA-256 / SHA-512 / HMAC — use the Teensy's built-in HashLib or port a portable C impl. Needed for PBKDF2-HMAC-SHA512 (mnemonic_to_seed), SHA-256 (event IDs), HMAC (auth envelope).
  3. ed25519 / x25519 — use a portable ed25519 (e.g. the ref10 impl or micro-ecc + portable ed25519). The CYD uses mbedtls/PSA; the Teensy doesn't have PSA, so we need a portable impl.
  4. PQClean — compile resources/pqclean/ (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) for Cortex-M7. The Keccak core is portable C. Verify keygen + sign + verify against the host's test vectors.
  5. nostr_core_lib — compile resources/nostr_core_lib/ (nip004, nip044, nostr_common, utils) for Cortex-M7. Portable C, should compile as-is.

Exit criterion: all crypto operations produce the same outputs as the host n_signer for the same inputs (cross-check with test vectors).

Phase 2: Key derivation + mnemonic

  1. BIP-39 wordlist — port firmware/cyd_esp32_2432s028/main/mnemonic_wordlist.h (2048 words). It's a static array, compiles as-is.
  2. mnemonic.cpp — port firmware/cyd_esp32_2432s028/main/mnemonic.c:
    • generate_mnemonic_12() — 16 random bytes → 12 words + checksum. Random source: Teensy's built-in TRNG (true random number generator).
    • validate_mnemonic() — checksum validation.
    • mnemonic_to_seed() — PBKDF2-HMAC-SHA512, 2048 iterations, 64-byte seed. Replace mbedtls/PSA calls with the portable SHA-512 from Phase 1.
  3. key_derivation.cpp — port firmware/cyd_esp32_2432s028/main/key_derivation.c:
    • seed → secp256k1 privkey (BIP-32 derivation, NIP-06).
    • seed → ed25519 / x25519 (SLIP-0010).
    • seed → PQ keypairs (ML-DSA-65, SLH-DSA-128s, ML-KEM-768).
    • Replace mbedtls/PSA calls with portable crypto from Phase 1.
  4. bech32.cpp — port firmware/cyd_esp32_2432s028/main/bech32.c for npub encoding.

Exit criterion: same mnemonic → same npub, same secp256k1/ed25519/x25519/PQ public keys as the CYD and the host n_signer.

Phase 3: Display + touch drivers

  1. display.cpp — thin wrapper around ST7796_t3:
    • display_init()tft.init(320, 480) + setRotation(1) (landscape 480×320, per the bring-up lessons).
    • display_fill(color) — full-screen fill.
    • display_fill_rect(x, y, w, h, color).
    • display_draw_text(x, y, text, color, bg) — uses ST7796_t3's text rendering. Monospaced, setTextSize(1) or (2) for readability.
    • display_draw_rect(x, y, w, h, color) — for borders.
    • display_clear() — fill black.
    • Constants: SCREEN_W=480, SCREEN_H=320 (NOT tft.width()/tft.height() — those are broken, see bring-up lessons).
  2. touch.cpp — port the XPT2046 driver from the touch_cal sketch:
    • touch_init() — pinMode T_CS=9, T_IRQ=2 (NOT pin 10!).
    • touch_read_raw() — hardware SPI at 2 MHz, 7-sample average, pressure threshold 80.
    • touch_read_screen() — calibrated screen coords using the constants from firmware/teensy41/README.md: x_min=207, x_max=1909, y_min=168, y_max=1798, invert_x=1, invert_y=1, with axis swap (raw Y → screen X, raw X → screen Y).
    • touch_tapped() — non-blocking: returns true if a tap completed since last call.

Exit criterion: can fill the screen, draw text, and read calibrated touch coords across the full 480×320 area.

Phase 4: UI screens (LVGL 8.3)

Port the CYD's LVGL screens from firmware/cyd_esp32_2432s028/main/ui.c. The CYD already uses the aesthetics color palette (black bg, white text, red pressed/focused, grey disabled) — see style_screen(), style_button(), style_key_matrix() in that file. We port those styles + screens, adapting for the 480×320 landscape (the CYD is 320×240 portrait).

  1. LVGL display + touch driver:

    • Install LVGL via arduino-cli lib install lvgl.
    • Write lvgl_flush_cb() — a lv_disp_drv_t flush callback that converts LVGL's lv_color_t pixels to RGB565 and pushes them to the ST7796S via tft.fillRect / tft.drawPixels. Template: the CYD's lvgl_flush_cb (swap ili9341_blit_rgb565 for ST7796_t3 calls).
    • Write lvgl_touch_read_cb() — a lv_indev_drv_t callback that calls our calibrated touch_read_screen() and feeds the coords to LVGL. Template: the CYD's lvgl_touch_read_cb.
    • LVGL tick: a timer interrupt or lv_tick_inc() call in loop().
    • lv_conf.h: configure for 480×320, 16-bit color, the draw buffer size (use DMAMEM for the buffer to keep it in PSRAM).
  2. Port the CYD screens (from firmware/cyd_esp32_2432s028/main/ui.c):

    • ui_startup_menu() → port the startup menu (Generate / Enter buttons). Adapt button positions for 480×320 landscape.
    • ui_show_mnemonic() → port the 12-word display grid. 3×4 or 4×3 layout for the wider screen.
    • ui_enter_mnemonic() → port the on-screen keyboard + BIP-39 suggestion logic. This is the most complex screen — the CYD's key matrix + prefix matching + suggestion row port directly, just reflow for 480×320.
    • ui_show_idle() → port the idle screen (npub + version). Wider screen gives more room for the npub.
    • ui_approve() → port the approval prompt (verb + summary + Approve/Deny buttons + 30s countdown).
  3. Aesthetics enforcement: the CYD's style_screen(), style_button(), and style_key_matrix() already use the correct palette:

    • bg = 0x000000 (black)
    • text = 0xFFFFFF (white)
    • pressed/focused = 0xFF0000 (red)
    • disabled = 0x505050 / 0x303030 (grey) Port these style functions as-is. They match ~/lt/aesthetics/WEB.md exactly.

Exit criterion: can walk through generate → show mnemonic → idle, and enter → idle, and approval → approve/deny, all via touch, with the black/white/red aesthetics.

Phase 5: USB CDC transport

  1. transport.cpp — port firmware/cyd_esp32_2432s028/main/uart_transport.c for USB CDC:
    • transport_init()Serial.begin(115200) (USB CDC, already verified in HelloSerial).
    • transport_read_frame(buf, max_len) — read 4-byte big-endian length prefix, then that many bytes of JSON-RPC payload. Returns the payload length or -1 on error/timeout.
    • transport_write_frame(buf, len) — write 4-byte big-endian length prefix
      • payload.
    • Non-blocking read with a timeout so the UI can still process touch events while waiting for a request.

Exit criterion: can round-trip a JSON-RPC request/response over USB CDC with a Python or C test client.

Phase 6: Verb dispatch

  1. dispatch.cpp — port handle_request() from firmware/cyd_esp32_2432s028/main/main.c (lines 1297+). This is the bulk of the logic:
    • Parse the JSON-RPC request (cJSON).
    • Verify the auth envelope (secp256k1 schnorr) if required.
    • Dispatch by verb: sign, get_public_key, derive, encrypt, decrypt, encapsulate, decapsulate, derive_shared_secret, verify, get_info.
    • For signing verbs: call ui_approve() and only proceed if approved.
    • Build the structured JSON result.
    • Replace all mbedtls/PSA calls with portable crypto from Phase 1.
  2. auth_envelope.cpp — port src/auth_envelope.c for the schnorr auth verify.
  3. otp_pad.cpp — port the HKDF-derived pad from the CYD (the Teensy's 1 TB SDXC pad is a future enhancement; for now, use the same HKDF-from-seed pad as the CYD).

Exit criterion: every verb produces the same result as the CYD for the same mnemonic + inputs.

Phase 7: Integration + end-to-end test

  1. teensy41_signer.ino — the Arduino entry:
    void setup() {
        display_init();
        touch_init();
        // Boot screen: "n_signer" logo, version.
        // Startup menu: generate or enter mnemonic.
        // Apply mnemonic → derive seed + keys → zeroize mnemonic.
        // Idle screen: show npub.
        // Transport init: USB CDC ready.
    }
    void loop() {
        // Read framed request (non-blocking, with touch polling).
        // Dispatch verb.
        // Write framed response.
        // Return to idle screen.
    }
    
  2. End-to-end smoke test:
    • Load a known mnemonic, verify the npub matches the CYD/host.
    • Exercise every verb over USB CDC with a Python test client.
    • Verify approval prompts appear for signing verbs and can be approved/denied via touch.
    • Power-cycle → mnemonic is gone, must re-enter.
  3. Cross-board parity: same mnemonic on Teensy 4.1 and CYD → same npub, same public keys, same signatures.

Exit criterion: the Teensy 4.1 is a fully functional n_signer hardware signer, interchangeable with the CYD.

Decisions (resolved 2026-07-26)

  • UI framework: LVGL 8.3. Port the CYD's LVGL screens from firmware/cyd_esp32_2432s028/main/ui.c. The Teensy has 1 MB SRAM + 16 MB PSRAM — plenty for LVGL. We need to write an LVGL display driver for ST7796_t3: a lv_disp_drv_t flush callback that pushes pixel data to the display via tft.fillRect / tft.drawPixels, and a lv_indev_drv_t touch callback that calls our calibrated touch_read_screen(). The CYD's lvgl_flush_cb and lvgl_touch_read_cb in firmware/cyd_esp32_2432s028/main/ui.c are the templates — swap the ili9341_blit_rgb565 call for ST7796_t3 calls, and the touch_read_screen call for our calibrated touch driver. LVGL is installed via Arduino Library Manager (arduino-cli lib install lvgl). Phase 4 is updated to reflect LVGL instead of hand-rolled primitives.
  • ed25519/x25519 impl: investigate whether the Teensy's mbedtls Arduino library has ed25519/x25519. If not, use a portable impl (orlp/ed25519 + micro-ecc for x25519).
  • OTP pad: for the initial port, use the HKDF-from-seed pad (same as CYD). The 1 TB SDXC pad is a separate enhancement after the 1 TB card arrives.
  • cJSON: vendor the same cJSON the CYD uses (it's a single .c/.h pair, public domain, compiles anywhere).