Files
n_signer/plans/teensy41_signer_implementation.md
T

326 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: Teensy 4.1 Signer Implementation
> Remaining correctness and performance work is tracked in
> [`teensy41_signer_remaining_fixes.md`](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`](https://github.com/...) (black/white/red, monospaced,
menu-first).
Phase 0 (bring-up) is complete — see [`teensy41_bringup.md`](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`](https://github.com/...)):
- 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
```mermaid
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
```cpp
// 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/`](../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/`](../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`](../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`](../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`](../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`](../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`](../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`](../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`](../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`](https://github.com/...) 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`](../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`](../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`](../../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:
```cpp
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`](../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`](../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).