326 lines
16 KiB
Markdown
326 lines
16 KiB
Markdown
# 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).
|