# Plan: Port n_signer to Teensy 4.1 with SDXC 1 TB exFAT OTP Pad ## Goal Port the n_signer hardware signer to the **Teensy 4.1** (NXP i.MX RT1062, Cortex-M7 @ 600 MHz), using its built-in SD slot to hold a **1 TB SDXC exFAT one-time-pad file**. The Teensy 4.1 is the high-capacity-OTP target; the CYD (classic ESP32) remains the cheap/low-power option with the mnemonic-derived stream pad. The on-the-wire protocol is identical to the host and the CYD/feather firmware ([`README.md`](../README.md) §4 — the algorithm-based API). The auth envelope, verb set, enforcement matrix, `key_id` convention, and structured-result JSON are all unchanged. ## Why the Teensy 4.1 | Concern | Teensy 4.1 | CYD (ESP32) | |---|---|---| | MCU | 600 MHz Cortex-M7 | 240 MHz Xtensa LX6 | | SRAM | 1 MB + 16 MB PSRAM (on-board) | 512 KB, no PSRAM | | SD slot | **4-bit SDMMC, exFAT via SdFat, up to 2 TB** | 1-bit SDSPI, FAT32, up to 32 GB | | SD speed | ~20-40 MB/s | ~2 MB/s | | USB | Hi-Speed (480 Mbps) device + host | CH340 UART only | | WiFi | **None** | Yes (unused) | | PQ crypto | ~1-2 s SLH-DSA-128s | 5-30 s SLH-DSA-128s | | Display | Add SPI ILI9341 (same panel as CYD) | Built-in 2.8" ILI9341 + touch | The decisive factor is the **SD slot**: PJRC's [SdFat](https://github.com/greiman/SdFat) library has native exFAT support, so a 1 TB SDXC card (which ships formatted exFAT) mounts and reads/writes directly — no reformatting, no exFAT driver work. The 4-bit SDMMC bus is fast enough (~20-40 MB/s) for pad reads with arbitrary seeks. ## Hardware ### Board - **Teensy 4.1** (PJRC) — $27. Has 8 MB flash, 16 MB external QSPI, 16 MB PSRAM (soldered), 1 MB internal SRAM, native USB Hi-Speed, built-in SD slot, 10/100 Ethernet PHY, no WiFi/BT. ### Display + touch (add-on) - **4.0" ST7796S 480×320 with XPT2046 resistive touch** (Hosyond or equivalent, ~$12-15). Specs: 4-wire SPI, RGB 65K, 3.3V~5V (works at Teensy's 3.3V logic), XPT2046 resistive touch, includes touch pen + SD card slot on the module. SPI wiring to the Teensy 4.1: - TFT: MOSI=pin 11, SCK=pin 13, MISO=pin 12, CS=pin 10, DC=pin 9, RESET=pin 8, BL=pin 22 (PWM via analogWrite) - Touch (XPT2046, shared SPI bus): T_CS=pin 7, T_IRQ=pin 6, T_CLK=pin 13, T_MOSI=pin 11, T_MISO=pin 12 - Power: VCC=3.3V, GND=GND - The ST7796S controller needs a different init sequence than the CYD's ILI9341, and the resolution is 480×320 (not 320×240). The XPT2046 touch driver ports from [`firmware/cyd_esp32_2432s028/main/touch.c`](../firmware/cyd_esp32_2432s028/main/touch.c) with new resolution constants. - The module's on-board SD card slot is a bonus (backup pad / offset file), but the 1 TB pad uses the Teensy's built-in SD slot (4-bit SDMMC, faster). ### SD card - **1 TB microSDXC** (exFAT, ~$60-80) in the Teensy's built-in slot. - The pad file (`/pad.bin`) + offset file (`/pad.offset`) live on this card. ### USB transport - The Teensy's native USB port (device mode) exposes a **CDC-ACM serial** + optional **WebUSB vendor** interface (TinyUSB composite), same framing as the feather/CYD (4-byte big-endian length prefix + JSON-RPC payload). - The host sees `/dev/ttyACM0` (Linux) or `COMx` (Windows). ## Target directory layout ``` firmware/teensy41/ ├── README.md (created) ├── teensy41_signer.ino (Arduino entry, or main.cpp for PlatformIO) ├── src/ │ ├── main.cpp (app loop: UI → transport → dispatch) │ ├── dispatch.cpp (verb dispatch — ported from cyd main.c handle_request) │ ├── dispatch.h │ ├── key_derivation.cpp (BIP-39 → seed → secp256k1/ed25519/x25519/PQ keys) │ ├── key_derivation.h │ ├── pq_crypto.cpp (PQClean wrappers: ml-dsa-65, slh-dsa-128s, ml-kem-768) │ ├── pq_crypto.h │ ├── otp_pad_sd.cpp (SDXC exFAT pad: mount, read, offset persistence) │ ├── otp_pad_sd.h │ ├── transport.cpp (USB CDC + length-prefix framing) │ ├── transport.h │ ├── display.cpp (ILI9341 driver — ported from cyd ili9341.c) │ ├── display.h │ ├── touch.cpp (XPT2046 driver — ported from cyd touch.c) │ ├── touch.h │ ├── ui.cpp (LVGL or hand-rolled UI — ported from cyd ui.c) │ ├── ui.h │ ├── secure_mem.cpp (zeroize helpers) │ ├── secure_mem.h │ ├── bech32.cpp (npub encoding) │ ├── bech32.h │ ├── mnemonic.cpp (BIP-39 wordlist + validation) │ ├── mnemonic.h │ └── mnemonic_wordlist.h ├── lib/ │ ├── secp256k1/ (libsecp256k1, built for ARM Cortex-M7) │ ├── pqclean/ (PQClean ML-DSA-65, SLH-DSA-128s, ML-KEM-768) │ ├── nostr_core_lib/ (symlink/copy of resources/nostr_core_lib) │ └── SdFat/ (PJRC SdFat with exFAT — via Arduino Library Manager) └── platformio.ini (or Arduino project config) ``` ## Architecture ```mermaid flowchart TD USB[Host USB CDC] --> Frame[transport.cpp
length-prefix framing] Frame --> Auth[auth envelope verify
secp256k1 schnorr] Auth -->|ok| Disp[dispatch.cpp
handle_request] Auth -->|fail| Err[auth error] Disp -->|nostr_*| NIP[Nostr verbs
secp256k1 NIP-06] Disp -->|alg verb| Alg[Algorithm verbs
algorithm + index] Alg -->|otp| OTP[otp_pad_sd.cpp
SDXC exFAT pad] Alg -->|secp256k1/ed25519/x25519/PQ| KD[key_derivation.cpp] OTP --> SD[(1 TB SDXC
exFAT
/pad.bin + /pad.offset)] KD --> UI[ui.cpp
approval prompt
ILI9341 + XPT2046] UI -->|approve| Exec[execute verb] Exec --> Resp[structured JSON result] Resp --> Frame ``` ## Implementation phases ### Phase 1: Board bring-up (display + touch + SD) 1. **Toolchain:** Arduino CLI + Teensyduino (simplest), or PlatformIO with the `teensy` platform. Verify `Blink` + `HelloSerial` compile and flash. 2. **ILI9341 display:** port [`firmware/cyd_esp32_2432s028/main/ili9341.c`](../firmware/cyd_esp32_2432s028/main/ili9341.c) to Teensy GPIO + SPI (use `SPI.beginTransaction` for 40 MHz HSPI). Exit criterion: fill screen + draw text. 3. **XPT2046 touch:** port [`firmware/cyd_esp32_2432s028/main/touch.c`](../firmware/cyd_esp32_2432s028/main/touch.c). Exit criterion: read touch coordinates, map to 320×240. 4. **SD card + exFAT:** install SdFat via Arduino Library Manager. Mount a 1 TB SDXC card (exFAT), write + read a test file. Exit criterion: `sd.begin(SdioConfig(FIFO_SDIO))` succeeds on a 1 TB card, `file.open` + `file.write` + `file.read` round-trips. ### Phase 2: Crypto stack port 1. **secp256k1:** build `libsecp256k1` for ARM Cortex-M7 (no ASM, pure C with `USE_NUM_NONE` / `USE_FIELD_INV_BUILTIN`). Verify schnorr sign/verify + ECDSA sign/verify against known test vectors. 2. **ed25519 / x25519:** use a portable ed25519 (e.g. the ref10 impl or `crypto_mbedtls` if mbedtls is available for Teensy — alternatively [`micro-ecc`](https://github.com/kmackay/micro-ecc) + a portable ed25519). Verify against the host's test vectors. 3. **PQClean:** compile `resources/pqclean/` (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) for Cortex-M7. The `crypto_backend` abstraction needs a Teensy backend (`crypto_backend_sdfat.c` or reuse the vendored Keccak from [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — the Keccak core is portable C). SHA-256/512 via SdFat's built-in or a portable impl. Verify keygen + sign + verify against the host. 4. **nostr_core_lib:** compile [`resources/nostr_core_lib/`](../resources/nostr_core_lib/) (nip004, nip044, nostr_common, utils, crypto) for Cortex-M7. This is portable C and should compile as-is. ### Phase 3: Key derivation + mnemonic 1. Port [`firmware/cyd_esp32_2432s028/main/key_derivation.c`](../firmware/cyd_esp32_2432s028/main/key_derivation.c) (BIP-32/SLIP-0010 derivation for all 6 algorithms). Replace mbedtls/PSA calls with the portable crypto from Phase 2. 2. Port [`firmware/cyd_esp32_2432s028/main/mnemonic.c`](../firmware/cyd_esp32_2432s028/main/mnemonic.c) (BIP-39 wordlist + validation + mnemonic_to_seed via PBKDF2-HMAC-SHA512). 3. Port [`firmware/cyd_esp32_2432s028/main/bech32.c`](../firmware/cyd_esp32_2432s028/main/bech32.c) (npub encoding). ### Phase 4: OTP pad from SDXC (the key feature) 1. **`otp_pad_sd.cpp`:** - `otp_pad_init()`: mount the SD card via `sd.begin(SdioConfig(FIFO_SDIO))`, open `/pad.bin` for reading, open `/pad.offset` for the persistent offset. Read the offset file on boot; if absent, start at 0. - `otp_pad_read(buf, len)`: seek to the current offset in `/pad.bin`, read `len` bytes, advance the offset. If the offset + len exceeds the file size, return an error (pad exhausted). - `otp_pad_persist_offset()`: write the current offset to `/pad.offset` after each `encrypt`/`decrypt` call (or batch: persist every N calls to reduce SD wear). - `otp_pad_zeroize()`: close files, zeroize in-RAM state. 2. **Wire into the `encrypt`/`decrypt` verbs:** replace the CYD's HKDF-derived in-RAM pad with `otp_pad_read()`. The wire contract is unchanged (`encrypt`/`decrypt` with `algorithm:"otp"`, base64 payload, `pad_offset` in the response). 3. **UI:** show "reading pad from SD…" during the read (the SD read is fast but the user should see activity). Show the pad offset + remaining bytes on the idle screen. ### Phase 5: Transport + dispatch + UI 1. **Transport (`transport.cpp`):** TinyUSB CDC-ACM + 4-byte length-prefix framing (same as [`firmware/cyd_esp32_2432s028/main/uart_transport.c`](../firmware/cyd_esp32_2432s028/main/uart_transport.c)). Optionally add a WebUSB vendor interface for browser transport (the Teensy's Hi-Speed USB makes this fast). 2. **Dispatch (`dispatch.cpp`):** port `handle_request()` from [`firmware/cyd_esp32_2432s028/main/main.c`](../firmware/cyd_esp32_2432s028/main/main.c) — the entire verb dispatch (all nostr_* + algorithm-based verbs, enforcement matrix, structured results, auth envelope verify). This is the bulk of the logic and ports nearly verbatim (only the crypto backend calls change). 3. **UI (`ui.cpp`):** port [`firmware/cyd_esp32_2432s028/main/ui.c`](../firmware/cyd_esp32_2432s028/main/ui.c) (LVGL 8.3 or hand-rolled). The UI screens are: startup menu → generate mnemonic → confirm mnemonic → enter mnemonic → idle (show npub + pad offset) → approval prompt. The Teensy's 600 MHz M7 makes LVGL snappy. ### Phase 6: Integration + testing 1. **End-to-end smoke test:** load a mnemonic, exercise every verb over USB CDC with a Python script or the Web Serial test page ([`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) works for any CDC device). 2. **OTP pad test:** place a known pad file on the 1 TB SDXC card, run `encrypt` + `decrypt` round-trips, verify the XOR is correct and the offset advances + persists across power cycles. 3. **Cross-board parity:** same mnemonic on the Teensy 4.1 and the CYD → same npub, same secp256k1/ed25519/x25519/PQ public keys, same signatures. 4. **Performance:** measure SLH-DSA-128s sign time (expect ~1-2 s vs 5-30 s on ESP32), ML-DSA-65 sign time (expect ~50 ms), SD pad read throughput. ## Open questions / decisions - **UI framework:** LVGL 8.3 (heavier, proven on CYD) vs hand-rolled (lighter, faster to port, no external dep). The Teensy has enough RAM for LVGL. **Lean toward LVGL** for consistency with the CYD. - **ed25519/x25519 impl:** mbedtls is available for Teensy via the `mbedtls` Arduino library, but PSA crypto is not. Options: (a) mbedtls ed25519 (if the Arduino mbedtls has it), (b) a portable ed25519 like [`orlp/ed25519`](https://github.com/orlp/ed25519) + micro-ecc for x25519, (c) libsodium for Teensy. **Investigate (a) first, fall back to (b).** - **Toolchain:** Arduino CLI + Teensyduino (simplest, SdFat + USB stack included) vs PlatformIO (better dependency management, CI-friendly). **Lean toward Arduino CLI** for the initial port, migrate to PlatformIO later. - **Offset persistence frequency:** writing `/pad.offset` to SD after every `encrypt`/`decrypt` is safe but wears the SD. **Batch: persist every 16 calls, and also on idle timeout.** If power is lost, at most 16 pad bytes are reused (acceptable for a signing device, not for a high-volume OTP channel). ## Verification - `arduino-cli compile` (or `pio run`) builds clean for `teensy:avr:teensy41`. - Flash to the Teensy 4.1, load a mnemonic, exercise every verb over USB CDC. - 1 TB SDXC card mounts, `/pad.bin` reads at >20 MB/s, offset persists across power cycles. - Same mnemonic → same keys as the CYD and the host n_signer. - SLH-DSA-128s signs in <3 s (vs 5-30 s on ESP32).