Files
n_signer/plans/teensy41_signer_port.md

13 KiB
Raw Permalink Blame History

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.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

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)

  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 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. 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 + 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 — 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/ (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 (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 (BIP-39 wordlist + validation + mnemonic_to_seed via PBKDF2-HMAC-SHA512).
  3. Port 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). 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 — 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 (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 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 + 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).