v0.1.0 - CYD firmware v0.0.2: algorithm-based API upgrade (all verbs, all algorithms), vendored Keccak/SHAKE + PSA ed25519/x25519 for IDF v5.4, Web Serial test page, CYD docs, Teensy 4.1 port plan (1TB SDXC OTP pad), brainstorming READMEs for BLE/IR/NFC/FPGA signer concepts
This commit is contained in:
306
plans/cyd_algorithm_api_upgrade.md
Normal file
306
plans/cyd_algorithm_api_upgrade.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Plan: Bring `firmware/cyd_esp32_2432s028` Up to Date with the Algorithm-Based API
|
||||
|
||||
## Context
|
||||
|
||||
The main project ([`src/dispatcher.c`](../src/dispatcher.c)) has fully migrated to the
|
||||
algorithm-based API documented in [`README.md`](../README.md) §4. The migration is marked
|
||||
COMPLETED in [`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md): legacy verb
|
||||
names are gone from the wire protocol, implementation, tests, clients, and docs.
|
||||
|
||||
The CYD firmware at [`firmware/cyd_esp32_2432s028/main/main.c`](../firmware/cyd_esp32_2432s028/main/main.c)
|
||||
was **not** updated and still speaks the **legacy verb API**:
|
||||
|
||||
| Firmware verb (current) | Main-project verb (target) |
|
||||
|--------------------------|----------------------------|
|
||||
| `get_public_key` (+ `nostr_index`) | split → `get_public_key` (+ `algorithm`) **and** `nostr_get_public_key` (+ `nostr_index`) |
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `nip04_encrypt` / `nip04_decrypt` | `nostr_nip04_encrypt` / `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` / `nip44_decrypt` | `nostr_nip44_encrypt` / `nostr_nip44_decrypt` |
|
||||
| _(missing)_ | `sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`, `derive`, `encrypt`/`decrypt` (otp), `nostr_mine_event` |
|
||||
|
||||
### Good news: the crypto primitives already exist
|
||||
|
||||
The firmware already has every underlying primitive needed — only the **dispatch layer**
|
||||
in [`main.c`](../firmware/cyd_esp32_2432s028/main/main.c) is stale:
|
||||
|
||||
- [`key_derivation.h`](../firmware/cyd_esp32_2432s028/main/key_derivation.h): `derive_nostr_key_index`, `derive_ed25519_key`, `derive_x25519_key`, `derive_ml_dsa_65_key`, `derive_slh_dsa_128s_key`, `derive_ml_kem_768_key`, `schnorr_sign32`, `ed25519_sign32`
|
||||
- [`pq_crypto_firmware.h`](../firmware/cyd_esp32_2432s028/main/pq_crypto_firmware.h): `fw_pq_ml_dsa_65_sign/verify`, `fw_pq_slh_dsa_128s_sign/verify`, `fw_pq_ml_kem_768_encaps/decaps`
|
||||
- [`nostr_core_lib`](../resources/nostr_core_lib) nip004/nip044 already linked for the Nostr verbs
|
||||
|
||||
### `key_id` convention
|
||||
|
||||
The main project defines `key_id` as the **first 16 hex characters of the public key**
|
||||
(see [`src/dispatcher.c`](../src/dispatcher.c) ~line 1788). The firmware must match this
|
||||
so clients can correlate keys across targets.
|
||||
|
||||
## Scope
|
||||
|
||||
**Target:** [`firmware/cyd_esp32_2432s028`](../firmware/cyd_esp32_2432s028) only.
|
||||
The feather_s3_tft firmware is explicitly out of scope for this pass (it has the same gap
|
||||
but will be handled separately).
|
||||
|
||||
## Architecture: dispatch flow after upgrade
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[recv frame] --> B[parse JSON-RPC]
|
||||
B --> C{auth envelope}
|
||||
C -->|fail| Z[auth error]
|
||||
C -->|ok| D{method}
|
||||
D -->|nostr_*| E[Nostr verb branch<br/>secp256k1 NIP-06<br/>nostr_index selector]
|
||||
D -->|alg verb| F[Algorithm verb branch<br/>algorithm + index selector]
|
||||
E --> G[derive_request_key<br/>nostr_index]
|
||||
F --> H{algorithm}
|
||||
H -->|secp256k1| H1[derive_nostr_key_index]
|
||||
H -->|ed25519| H2[derive_ed25519_key]
|
||||
H -->|x25519| H3[derive_x25519_key]
|
||||
H -->|ml-dsa-65| H4[derive_ml_dsa_65_key]
|
||||
H -->|slh-dsa-128s| H5[derive_slh_dsa_128s_key]
|
||||
H -->|ml-kem-768| H6[derive_ml_kem_768_key]
|
||||
H -->|otp| H7[bound pad]
|
||||
G --> I[enforcement matrix check]
|
||||
H1 --> I
|
||||
H2 --> I
|
||||
H3 --> I
|
||||
H4 --> I
|
||||
H5 --> I
|
||||
H6 --> I
|
||||
H7 --> I
|
||||
I -->|reject 1010| Z
|
||||
I -->|ok| J[approval prompt]
|
||||
J --> K[execute verb]
|
||||
K --> L[structured result JSON<br/>algorithm + key_id + field]
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Add algorithm-name parsing helpers (`main.c`)
|
||||
|
||||
Add a small enum + parser mirroring the main project's algorithm set:
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
FW_ALG_SECP256K1 = 0,
|
||||
FW_ALG_ED25519,
|
||||
FW_ALG_X25519,
|
||||
FW_ALG_ML_DSA_65,
|
||||
FW_ALG_SLH_DSA_128S,
|
||||
FW_ALG_ML_KEM_768,
|
||||
FW_ALG_OTP,
|
||||
FW_ALG_UNKNOWN
|
||||
} fw_alg_t;
|
||||
```
|
||||
|
||||
- `parse_algorithm_from_options(cJSON *options, fw_alg_t *out_alg, uint32_t *out_index)`
|
||||
— reads `algorithm` (string) and `index` (number, default 0) from the trailing options
|
||||
object in `params`.
|
||||
- `fw_alg_name(fw_alg_t)` → canonical string (`"secp256k1"`, `"ed25519"`, …) for response
|
||||
JSON.
|
||||
- Keep the existing `parse_nostr_index_from_params()` for the `nostr_*` verbs.
|
||||
|
||||
### 2. Add a unified algorithm-key derivation + `key_id` helper
|
||||
|
||||
Add `derive_alg_key(fw_alg_t alg, uint32_t index, ...)` that dispatches to the right
|
||||
`derive_*_key` function and produces:
|
||||
- the raw private key bytes (when applicable),
|
||||
- the public key hex,
|
||||
- the `key_id` (first 16 hex chars of the public key).
|
||||
|
||||
PQ algorithms have large key buffers (ml-dsa-65 sk = 4032 B, ml-kem-768 sk = 2400 B).
|
||||
Allocate these as **static** buffers (not on the stack) and `secure_memzero` after use,
|
||||
matching the existing `s_privkey`/`s_pubkey` pattern. SLH-DSA-128s keygen is slow
|
||||
(5–30 s) — log a warning and show a "deriving key…" UI screen before calling it.
|
||||
|
||||
### 3. Add a structured-result builder
|
||||
|
||||
Add `build_alg_result_json(const char *alg_name, const char *key_id_16hex, const char *field_name, const char *field_value)` → returns a JSON string like
|
||||
`{"algorithm":"ed25519","key_id":"<16hex>","public_key":"<hex>"}`. This mirrors
|
||||
[`build_alg_result_json`](../src/dispatcher.c:861) in the main dispatcher.
|
||||
|
||||
### 4. Add the enforcement matrix
|
||||
|
||||
Add `enforce_alg_verb(fw_alg_t alg, const char *verb)` returning 0 / `1010`, matching
|
||||
[`README.md`](../README.md) §4.3 enforcement matrix:
|
||||
|
||||
| Verb | Valid algorithms |
|
||||
|------|------------------|
|
||||
| `sign` / `verify` | secp256k1, ed25519, ml-dsa-65, slh-dsa-128s |
|
||||
| `encapsulate` / `decapsulate` | ml-kem-768 |
|
||||
| `derive_shared_secret` | x25519 |
|
||||
| `derive` | secp256k1 |
|
||||
| `encrypt` / `decrypt` | otp |
|
||||
| `get_public_key` | all key-deriving algorithms |
|
||||
|
||||
Reject any unlisted pair with
|
||||
`{"error":{"code":1010,"message":"algorithm_not_supported_for_verb"}}`.
|
||||
|
||||
### 5. Rename the Nostr verbs (in-place, no compat shim)
|
||||
|
||||
In the dispatch `if/else if` chain in [`main.c`](../firmware/cyd_esp32_2432s028/main/main.c)
|
||||
~lines 836–1230:
|
||||
|
||||
- `get_public_key` (nostr_index branch) → `nostr_get_public_key`
|
||||
- `sign_event` → `nostr_sign_event`
|
||||
- `nip04_encrypt` → `nostr_nip04_encrypt`
|
||||
- `nip04_decrypt` → `nostr_nip04_decrypt`
|
||||
- `nip44_encrypt` → `nostr_nip44_encrypt`
|
||||
- `nip44_decrypt` → `nostr_nip44_decrypt`
|
||||
|
||||
The `nostr_get_public_key` verb should also honor the `format` option
|
||||
(`"structured"` → `{"algorithm":"secp256k1","public_key":"<hex>","key_id":"<16hex>"}`,
|
||||
default → bare 64-hex pubkey string), matching the main project.
|
||||
|
||||
### 6. Add the algorithm-based `get_public_key` verb
|
||||
|
||||
`get_public_key` with `algorithm` + `index` → derive the key, return structured JSON:
|
||||
`{"algorithm":"<alg>","public_key":"<hex>","key_id":"<16hex>"}`. For PQ algorithms the
|
||||
`public_key` is the full PQClean pubkey hex (1952 B for ml-dsa-65, 1184 B for ml-kem-768,
|
||||
32 B for slh-dsa-128s) — ensure `s_response_buf` (currently 2048 B) is large enough, or
|
||||
emit via `cJSON_PrintUnformatted` into a larger static buffer.
|
||||
|
||||
### 7. Add `sign` and `verify` (algorithm-based)
|
||||
|
||||
- `sign`: params `[<message_hex>, {algorithm, index, scheme?}]`. `scheme` is
|
||||
secp256k1-only (`"schnorr"` default / `"ecdsa"`). For ed25519 use `ed25519_sign32`
|
||||
(note: ed25519 signs the raw 32-byte message, not a pre-hash — match main project
|
||||
behavior). For ml-dsa-65 / slh-dsa-128s use `fw_pq_*_sign`. Response:
|
||||
`{"algorithm":"<alg>","key_id":"<16hex>","signature":"<hex>"}`.
|
||||
- `verify`: params `[<message_hex>, <signature_hex>, {algorithm, index, scheme?}]`.
|
||||
Derive the signer's own pubkey and verify against it. Response:
|
||||
`{"valid":true,"algorithm":"<alg>"}`.
|
||||
|
||||
### 8. Add `encapsulate` / `decapsulate` (ml-kem-768)
|
||||
|
||||
- `encapsulate`: params `[<peer_pubkey_hex>, {algorithm:"ml-kem-768"}]` →
|
||||
`fw_pq_ml_kem_768_encaps` →
|
||||
`{"ciphertext":"<hex>","shared_secret":"<hex>","algorithm":"ml-kem-768"}`.
|
||||
- `decapsulate`: params `[<ciphertext_hex>, {algorithm:"ml-kem-768", index}]` → derive
|
||||
kem keypair, `fw_pq_ml_kem_768_decaps` →
|
||||
`{"shared_secret":"<hex>","algorithm":"ml-kem-768"}`.
|
||||
|
||||
### 9. Add `derive_shared_secret` (x25519)
|
||||
|
||||
params `[<peer_pubkey_hex>, {algorithm:"x25519", index}]` → derive x25519 keypair,
|
||||
compute X25519 ECDH via mbedtls →
|
||||
`{"shared_secret":"<hex>","algorithm":"x25519"}`.
|
||||
|
||||
### 10. Add `derive` (secp256k1 HMAC-SHA256)
|
||||
|
||||
params `[<data>, {algorithm:"secp256k1", index}]` (`index` **required**) → derive
|
||||
secp256k1 privkey, compute `HMAC-SHA256(privkey, data)` via mbedtls →
|
||||
`{"algorithm":"secp256k1","key_id":"<16hex>","digest":"<64hex>"}`. See
|
||||
[`plans/derive_hmac.md`](../plans/derive_hmac.md) for the spec.
|
||||
|
||||
### 11. Add `nostr_mine_event` (PoW)
|
||||
|
||||
params `[<event_json>, {nostr_index, difficulty, timeout_sec, threads?}]`. Reuse
|
||||
`build_signed_event_json` but iterate nonce in the event's `tags` until the leading-zero
|
||||
bits of the event id meet `difficulty`. ESP32 is slow — cap `threads` at 1 and enforce a
|
||||
firm `timeout_sec` (default 30). Show a "mining…" UI screen. If timeout, return error
|
||||
`1008 mining_failed`. This mirrors [`src/miner.c`](../src/miner.c).
|
||||
|
||||
### 12. Add `encrypt` / `decrypt` (otp)
|
||||
|
||||
The main project binds a pad from `--otp-pad-dir` + `--otp-pad` (a USB file). The CYD has
|
||||
no filesystem pad source. **Decision: derive the OTP pad from the mnemonic seed** via a
|
||||
SHAKE-256 / HKDF expansion keyed on `algorithm:"otp"` so the pad is deterministic per
|
||||
mnemonic and advances monotonically across requests (offset stored in a static variable,
|
||||
reported in every response). This keeps the wire contract identical (`encrypt`/`decrypt`
|
||||
with `algorithm:"otp"`, `encoding:"ascii"|""binary""`) while fitting the embedded
|
||||
constraint. Document this divergence in [`firmware/README.md`](../firmware/README.md).
|
||||
|
||||
### 13. Bump `FIRMWARE_VERSION` and update `firmware/README.md`
|
||||
|
||||
- `FIRMWARE_VERSION` "0.0.1" → "0.0.2" (algorithm-based API).
|
||||
- Document the new verb table, the OTP pad-derivation divergence, and the SLH-DSA-128s
|
||||
latency warning.
|
||||
|
||||
### 14. Update CYD-targeting examples / clients
|
||||
|
||||
Audit and update any example or client that speaks to the CYD over UART/Web-Serial and
|
||||
uses legacy verb names:
|
||||
- [`examples/feather_get_public_key.py`](../examples/feather_get_public_key.py) and
|
||||
[`examples/feather_sign_event.py`](../examples/feather_sign_event.py) (feather-targeting
|
||||
but the wire protocol is shared — note in README they need `nostr_` prefixes for CYD
|
||||
after this change; leave feather examples alone since feather is out of scope, but add a
|
||||
CYD-specific example pair if none exists).
|
||||
- [`client/`](../client/) demos already use the new verbs (per
|
||||
[`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md)) — verify no CYD-specific
|
||||
legacy calls remain.
|
||||
|
||||
### 15. Add a CYD Web Serial test page covering all algorithms
|
||||
|
||||
The existing [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html) is
|
||||
**WebUSB-only** (feather's native USB) and uses the **legacy verbs**. The CYD's CH340
|
||||
bridge (`1a86:7523`) is not a WebUSB device — it exposes a serial port, so the browser
|
||||
transport is **Web Serial** (`navigator.serial`), Chromium-only.
|
||||
|
||||
Create [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) — a
|
||||
single-file, dependency-light test page that:
|
||||
|
||||
**Transport:**
|
||||
- `navigator.serial.requestPort()` → `port.open({ baudRate: 115200 })` (matches
|
||||
[`uart_transport.c`](../firmware/cyd_esp32_2432s028/main/uart_transport.c) UART_BAUD_RATE).
|
||||
- Same 4-byte big-endian length-prefix frame format as the feather demo
|
||||
([`be32()`](../examples/feather_webusb_demo.html:256), read loop reassembling frames).
|
||||
- Read via a `ReadableStream` reader + length-prefix reassembly (Web Serial is stream-based,
|
||||
not packet-based like WebUSB `transferIn`).
|
||||
- Same auth-envelope construction (kind 27235, `nsigner_method` / `nsigner_body_hash` tags,
|
||||
schnorr sign with a demo caller key) — reuse the
|
||||
[`buildAuth()`](../examples/feather_webusb_demo.html:279) logic verbatim.
|
||||
|
||||
**UI sections (one card per verb family, all algorithms):**
|
||||
1. **Connect** — Connect Web Serial button + status.
|
||||
2. **Get Public Key** — algorithm dropdown (`secp256k1`, `ed25519`, `x25519`, `ml-dsa-65`,
|
||||
`slh-dsa-128s`, `ml-kem-768`) + index → `get_public_key`. Also a `nostr_get_public_key`
|
||||
card with `nostr_index` + `format` (bare / structured) toggle.
|
||||
3. **Sign / Verify** — algorithm dropdown (sig algs only) + index + `scheme` (schnorr/ecdsa,
|
||||
secp256k1-only) + message hex → `sign`; then `verify` with the returned signature.
|
||||
4. **KEM (ml-kem-768)** — `encapsulate` with a peer pubkey (or self-pubkey from
|
||||
`get_public_key`) → ciphertext + shared secret; `decapsulate` with that ciphertext →
|
||||
shared secret (confirm match).
|
||||
5. **X25519** — `derive_shared_secret` with peer pubkey.
|
||||
6. **Derive (HMAC)** — `derive` with data string + index → 64-hex digest.
|
||||
7. **Nostr Sign Event** — `nostr_sign_event` (kind 1) with `nostr_index`.
|
||||
8. **Nostr Mine Event** — `nostr_mine_event` with difficulty + timeout (low default, e.g.
|
||||
difficulty 4) — warn it's slow on ESP32.
|
||||
9. **NIP-04 / NIP-44** — `nostr_nip04_encrypt`/`decrypt`, `nostr_nip44_encrypt`/`decrypt`
|
||||
with `nostr_index`.
|
||||
10. **OTP** — `encrypt` / `decrypt` with `algorithm:"otp"`, `encoding` toggle
|
||||
(ascii/binary), base64 plaintext.
|
||||
|
||||
Each card shows the raw JSON-RPC request and response in a `<pre>` so the wire format is
|
||||
visible. Reuse the feather demo's CSS (dark theme, cards, `.mono` log) for consistency.
|
||||
|
||||
**Link it from [`firmware/README.md`](../firmware/README.md)** in the CYD section (the
|
||||
"Quick validation" / Web Serial path), since the current README only points at the
|
||||
feather WebUSB demo.
|
||||
|
||||
### 16. Verification
|
||||
|
||||
- `idf.py build` in [`firmware/cyd_esp32_2432s028`](../firmware/cyd_esp32_2432s028) compiles
|
||||
clean.
|
||||
- Flash to the connected CYD board (CH340 on `/dev/ttyUSB0`) and smoke-test each verb:
|
||||
- Primary path: open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html)
|
||||
in Chrome/Edge, connect via Web Serial, exercise every card.
|
||||
- Secondary path: a small Python script over `/dev/ttyUSB0` for headless confirmation.
|
||||
- Cover: `get_public_key` (each algorithm), `sign`/`verify` (each sig alg),
|
||||
`encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`,
|
||||
`nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_encrypt`/`decrypt`,
|
||||
`nostr_nip44_encrypt`/`decrypt`, `nostr_mine_event` (low difficulty),
|
||||
`encrypt`/`decrypt` (otp).
|
||||
- Confirm `key_id` matches the first 16 hex of the returned pubkey for every alg.
|
||||
- Confirm an invalid `(verb, algorithm)` pair returns code 1010.
|
||||
- `grep -rn "sign_event\|nip04_encrypt\|nip04_decrypt\|nip44_encrypt\|nip44_decrypt"
|
||||
firmware/cyd_esp32_2432s028/` returns no matches (legacy names gone).
|
||||
|
||||
## Open questions / decisions baked in
|
||||
|
||||
- **OTP pad source:** derived from mnemonic (no USB pad on CYD). Documented divergence.
|
||||
- **`nostr_mine_event`:** implemented, single-threaded, hard 30 s default timeout, with a
|
||||
"mining…" UI screen. Not stubbed — the main project has it and the user asked to bring
|
||||
the firmware up to date.
|
||||
- **No legacy-verb compat shim:** matches the main project's policy
|
||||
([`plans/legacy_verb_aliases.md`](../plans/legacy_verb_aliases.md) — "No backward-
|
||||
compatibility shim is needed").
|
||||
- **feather_s3_tft not touched** in this pass.
|
||||
248
plans/teensy41_signer_port.md
Normal file
248
plans/teensy41_signer_port.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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<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`](../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).
|
||||
Reference in New Issue
Block a user