13 KiB
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 §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 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.3V5V (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.cwith 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) orCOMx(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
flowchart TD
USB[Host USB CDC] --> Frame[transport.cpp<br/>length-prefix framing]
Frame --> Auth[auth envelope verify<br/>secp256k1 schnorr]
Auth -->|ok| Disp[dispatch.cpp<br/>handle_request]
Auth -->|fail| Err[auth error]
Disp -->|nostr_*| NIP[Nostr verbs<br/>secp256k1 NIP-06]
Disp -->|alg verb| Alg[Algorithm verbs<br/>algorithm + index]
Alg -->|otp| OTP[otp_pad_sd.cpp<br/>SDXC exFAT pad]
Alg -->|secp256k1/ed25519/x25519/PQ| KD[key_derivation.cpp]
OTP --> SD[(1 TB SDXC<br/>exFAT<br/>/pad.bin + /pad.offset)]
KD --> UI[ui.cpp<br/>approval prompt<br/>ILI9341 + XPT2046]
UI -->|approve| Exec[execute verb]
Exec --> Resp[structured JSON result]
Resp --> Frame
Implementation phases
Phase 1: Board bring-up (display + touch + SD)
- Toolchain: Arduino CLI + Teensyduino (simplest), or PlatformIO with the
teensyplatform. VerifyBlink+HelloSerialcompile and flash. - ILI9341 display: port
firmware/cyd_esp32_2432s028/main/ili9341.cto Teensy GPIO + SPI (useSPI.beginTransactionfor 40 MHz HSPI). Exit criterion: fill screen + draw text. - XPT2046 touch: port
firmware/cyd_esp32_2432s028/main/touch.c. Exit criterion: read touch coordinates, map to 320×240. - 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.openfile.write+file.readround-trips.
Phase 2: Crypto stack port
- secp256k1: build
libsecp256k1for ARM Cortex-M7 (no ASM, pure C withUSE_NUM_NONE/USE_FIELD_INV_BUILTIN). Verify schnorr sign/verify + ECDSA sign/verify against known test vectors. - ed25519 / x25519: use a portable ed25519 (e.g. the ref10 impl or
crypto_mbedtlsif mbedtls is available for Teensy — alternativelymicro-ecc+ a portable ed25519). Verify against the host's test vectors. - PQClean: compile
resources/pqclean/(ML-DSA-65, SLH-DSA-128s, ML-KEM-768) for Cortex-M7. Thecrypto_backendabstraction needs a Teensy backend (crypto_backend_sdfat.cor reuse the vendored Keccak fromresources/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. - nostr_core_lib: compile
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
- Port
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. - Port
firmware/cyd_esp32_2432s028/main/mnemonic.c(BIP-39 wordlist + validation + mnemonic_to_seed via PBKDF2-HMAC-SHA512). - Port
firmware/cyd_esp32_2432s028/main/bech32.c(npub encoding).
Phase 4: OTP pad from SDXC (the key feature)
otp_pad_sd.cpp:otp_pad_init(): mount the SD card viasd.begin(SdioConfig(FIFO_SDIO)), open/pad.binfor reading, open/pad.offsetfor 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, readlenbytes, 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.offsetafter eachencrypt/decryptcall (or batch: persist every N calls to reduce SD wear).otp_pad_zeroize(): close files, zeroize in-RAM state.
- Wire into the
encrypt/decryptverbs: replace the CYD's HKDF-derived in-RAM pad withotp_pad_read(). The wire contract is unchanged (encrypt/decryptwithalgorithm:"otp", base64 payload,pad_offsetin the response). - 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
- Transport (
transport.cpp): TinyUSB CDC-ACM + 4-byte length-prefix framing (same asfirmware/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). - Dispatch (
dispatch.cpp): porthandle_request()fromfirmware/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). - UI (
ui.cpp): portfirmware/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
- 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.htmlworks for any CDC device). - OTP pad test: place a known pad file on the 1 TB SDXC card, run
encrypt+decryptround-trips, verify the XOR is correct and the offset advances + persists across power cycles. - Cross-board parity: same mnemonic on the Teensy 4.1 and the CYD → same npub, same secp256k1/ed25519/x25519/PQ public keys, same signatures.
- 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
mbedtlsArduino library, but PSA crypto is not. Options: (a) mbedtls ed25519 (if the Arduino mbedtls has it), (b) a portable ed25519 likeorlp/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.offsetto SD after everyencrypt/decryptis 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(orpio run) builds clean forteensy:avr:teensy41.- Flash to the Teensy 4.1, load a mnemonic, exercise every verb over USB CDC.
- 1 TB SDXC card mounts,
/pad.binreads 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).