# Feather S3 TFT Firmware This directory contains the ESP-IDF firmware for the Feather S3 TFT signer target. ## USB transport mode Firmware now runs in **TinyUSB composite mode** (single USB peripheral owned by TinyUSB), exposing: - CDC-ACM interface for Python/serial tooling (`/dev/ttyACM*`) - Vendor interface for WebUSB browser transport Expected runtime USB identity: - VID:PID `303a:4001` - Product string: `Feather S3 TFT n_signer` ## Flashing workflow (important) Because TinyUSB owns USB at runtime, normal app mode is not the ROM USB-Serial/JTAG flashing interface. Use this workflow for flashing: 1. Enter bootloader mode: hold **BOOT**, tap **RESET**, release **BOOT**. 2. Confirm bootloader USB device appears (typically `/dev/ttyACM0`). 3. Flash: ```bash cd firmware/feather_s3_tft idf.py -p /dev/ttyACM0 flash ``` 4. Exit bootloader and run app: ensure BOOT is released, then tap **RESET** once. 5. Confirm app USB identity: ```bash lsusb | grep -i 303a # expect: 303a:4001 n_signer Feather S3 TFT n_signer ``` ## Quick validation CDC path: ```bash ./.venv/bin/python ./examples/feather_get_public_key.py /dev/ttyACM0 ./.venv/bin/python ./examples/feather_sign_event.py /dev/ttyACM0 ``` WebUSB path: - Open [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html) - Connect in Chrome/Edge - Run `get_public_key` - Confirm pubkey matches CDC result ## CYD (ESP32-2432S028) validation — Web Serial The CYD has no native USB; its CH340 bridge exposes a serial port. The browser transport is **Web Serial** (`navigator.serial`), Chromium-only. A full test page covering every algorithm and verb lives at [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html): - Open [`examples/cyd_webserial_demo.html`](../examples/cyd_webserial_demo.html) in Chrome/Edge - Click **Connect Web Serial**, select the CH340 port (`1a86:7523`) - Exercise each card: `get_public_key` (all 6 algorithms), `sign`/`verify`, `encapsulate`/`decapsulate`, `derive_shared_secret`, `derive`, `nostr_get_public_key`, `nostr_sign_event`, `nostr_mine_event`, `nostr_nip04`/`nostr_nip44` encrypt+decrypt, and `encrypt`/`decrypt` (otp) - Each request shows the raw JSON-RPC request and response The CYD firmware (v0.0.2+) speaks the same algorithm-based API as the host ([`README.md`](../README.md) §4). The OTP pad is derived from the mnemonic seed (no USB pad on this board); the offset advances monotonically and is reported in every `encrypt`/`decrypt` response. ## Linux WebUSB host setup (one-time) Chrome and Edge need permission to open the device on Linux. Install a udev rule for the firmware VID:PID and reload rules: ```bash sudo tee /etc/udev/rules.d/99-nsigner-webusb.rules >/dev/null <<'EOF' SUBSYSTEM=="usb", ATTR{idVendor}=="303a", ATTR{idProduct}=="4001", MODE="0666", GROUP="plugdev", TAG+="uaccess" EOF sudo udevadm control --reload-rules sudo udevadm trigger ``` After installing the rule, unplug and replug the device once, then reload the demo page. ## Troubleshooting - If `lsusb` shows `303a:1001`, the board is still in ROM bootloader mode. Tap RESET with BOOT released. - If CDC command hangs, verify app-mode VID:PID is `303a:4001`, not `303a:1001`. - If no `/dev/ttyACM*` appears, unplug/replug after reset and re-check `lsusb`. - If WebUSB connect throws `SecurityError: Access denied`, install the Linux udev rule above and replug. ## CYD ESP32-2432S028 (CH340) serial-reset hardware note For the CYD board (`firmware/cyd_esp32_2432s028`), opening `/dev/ttyUSB0` can reset the ESP32 because CH340 DTR/RTS control lines are wired into the ESP32 auto-reset circuit. Symptoms: - Device returns to startup/menu when a host app opens serial - Browser/CLI connect appears to "knock" signer out of ready screen Field-proven mitigation: - Add a **10uF capacitor between EN and GND** on the CYD board - A practical solder point is across the board reset path (e.g. reset button EN/GND pads) - For polarized electrolytic caps: **negative leg to GND**, positive to EN Expected result after this hardware mod: - Serial open no longer triggers unwanted reset in normal host workflows - Signer remains in ready state across browser/CLI connect events Notes: - Typical working range is ~4.7uF to 22uF; 10uF is recommended - Keep leads short for best stability - Auto-reset behavior for flashing may still work, but if flashing ever becomes unreliable, enter bootloader manually ## Post-quantum crypto support (Phase 7) Both firmware targets (`feather_s3_tft` and `cyd_esp32_2432s028`) now include the three NIST-standardized post-quantum algorithms alongside the existing secp256k1 (Nostr) and new ed25519/x25519 classical algorithms: | Algorithm | Standard | Purpose | Pub key | Priv key | Sig/Ct | |---|---|---|---|---|---| | secp256k1 | — | Nostr (existing) | 32 B | 32 B | 64 B | | ed25519 | RFC 8032 | SSH signatures | 32 B | 32 B | 64 B | | x25519 | RFC 7748 | Key agreement (age) | 32 B | 32 B | — | | ML-DSA-65 | FIPS 204 | PQ signatures | 1952 B | 4032 B | 3309 B | | SLH-DSA-128s | FIPS 205 | PQ hash-based sigs | 32 B | 64 B | 7856 B | | ML-KEM-768 | FIPS 203 | PQ key encapsulation | 1184 B | 2400 B | 1088 B | ### mbedtls backend (vs OpenSSL on host) The host build uses OpenSSL EVP for SHA-256, SHA-512, SHA3-256, SHA3-512, SHAKE-128, and SHAKE-256. On ESP32, OpenSSL is not available. Instead, the firmware uses a **crypto backend abstraction** ([`resources/pqclean/common/crypto_backend.h`](../resources/pqclean/common/crypto_backend.h)) with two implementations: - [`resources/pqclean/common/crypto_backend_openssl.c`](../resources/pqclean/common/crypto_backend_openssl.c) — host build (OpenSSL EVP) - [`resources/pqclean/common/crypto_backend_mbedtls.c`](../resources/pqclean/common/crypto_backend_mbedtls.c) — ESP32 firmware (mbedtls + vendored Keccak) The mbedtls backend uses: - `mbedtls_sha256()` for SHA-256 (ESP32 hardware accelerated where available) - `mbedtls_sha512()` for SHA-512 (ESP32 hardware accelerated where available) - A **self-contained Keccak-f[1600]** implementation (FIPS 202, public domain) for SHA3-256, SHA3-512, SHAKE-128, and SHAKE-256. This is vendored directly in `crypto_backend_mbedtls.c` because ESP-IDF v5.x mbedtls does not expose SHAKE (and SHA3 is only available when `CONFIG_MBEDTLS_SHA3_C` is set) through the `mbedtls_md` API. Carrying the Keccak core avoids any mbedtls config dependency for the PQ algorithms. ### ed25519 / x25519 via PSA crypto ESP-IDF v5.x mbedtls removed the low-level `mbedtls_ed25519_*` functions. The firmware uses the **PSA Crypto API** for ed25519 sign/verify/key-derivation and x25519 key derivation + ECDH. Enable PSA in `sdkconfig.defaults`: ``` CONFIG_MBEDTLS_PSA_CRYPTO_C=y CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y ``` ### No SHA3/SHAKE menuconfig requirement Because SHA3/SHAKE are provided by the vendored Keccak core (not mbedtls), you do **not** need to enable `CONFIG_MBEDTLS_SHA3_C` or any SHAKE config. The PQ algorithms build and run with the default mbedtls configuration. ### PQClean component The PQClean algorithm code is compiled as an ESP-IDF component at `components/pqclean/`. The component's `CMakeLists.txt` references the shared source files in [`resources/pqclean/`](../resources/pqclean/) via relative paths, so there is a single source of truth for both host and firmware builds. The component includes: - ML-DSA-65: `sign.c`, `poly.c`, `ntt.c` - SLH-DSA-128s: `sign.c`, `fors.c`, `wots.c`, `hash.c`, `thash.c`, `address.c`, `utils.c` - ML-KEM-768: `kem.c`, `indcpa.c`, `poly.c`, `ntt.c`, `cbd.c`, `reduce.c`, `symmetric.c`, `verify.c` - Common: `fips202.c`, `sha2.c`, `crypto_backend_mbedtls.c` - Firmware DRBG: `pq_drbg_firmware.c`, `randombytes_mbedtls.c` ### Flash usage estimates | Algorithm | Code size (approx) | |---|---| | ML-DSA-65 | ~150 KB | | SLH-DSA-128s | ~80 KB | | ML-KEM-768 | ~120 KB | | Total PQ code | ~350 KB | The ESP32-S3 (Feather S3 TFT) has 8 MB flash and the ESP32 (CYD) has 4 MB flash. The PQ code fits comfortably in both, but partition sizes may need adjustment if the total app image exceeds the default partition. ### RAM usage notes PQ key buffers are large compared to classical ECC keys: | Buffer | Size | |---|---| | ML-DSA-65 private key | 4032 bytes | | ML-DSA-65 public key | 1952 bytes | | ML-DSA-65 signature | 3309 bytes | | SLH-DSA-128s signature | 7856 bytes | | ML-KEM-768 private key | 2400 bytes | | ML-KEM-768 public key | 1184 bytes | | ML-KEM-768 ciphertext | 1088 bytes | The ESP32 has ~320 KB available heap (after WiFi/BT are disabled). These buffers **must not be stack-allocated** — the default task stack is 8 KB. Use `malloc()` or static buffers. The firmware derives PQ keys **on demand** (not all at startup) to keep peak RAM usage low. ### SLH-DSA-128s signing latency warning SLH-DSA-128s (SPHINCS+-128s) is a hash-based signature scheme with a deep hypertree structure (7 layers of WOTS+ + Merkle trees). On the ESP32-S3 (240 MHz dual-core), expect: - **Key generation**: 5–30 seconds - **Signing**: 5–30 seconds - **Verification**: 0.5–2 seconds This is inherent to the algorithm — it trades computation for minimal trust assumptions (only SHA-256). The firmware logs a warning before SLH-DSA-128s keygen/signing. Users should choose whether to use SLH-DSA-128s per-role based on their latency tolerance. ML-DSA-65 is much faster (~100 ms for signing on ESP32-S3) and is the recommended PQ signature algorithm for interactive use. ### Derivation paths All algorithms derive from the mnemonic using BIP-32/HMAC-SHA512 with SLIP-0010 all-hardened derivation for ed25519/x25519/PQ: | Algorithm | Path | Notes | |---|---|---| | secp256k1 (Nostr) | `m/44'/1237'/'/0/0` | NIP-06, existing | | ed25519 (SSH) | `m/44'/102001'/'/0'/0'` | SLIP-0010 | | x25519 (age) | `m/44'/102002'/'/0'/0'` | SLIP-0010 | | ML-DSA-65 | `m/44'/102003'/'/0'/0'` | seed → PQClean keygen | | SLH-DSA-128s | `m/44'/102004'/'/0'/0'` | seed → PQClean keygen | | ML-KEM-768 | `m/44'/102005'/'/0'/0'` | seed → PQClean keygen | The PQ derivation produces a 32-byte seed that feeds a deterministic SHAKE-256 DRBG ([`pq_drbg_firmware.c`](feather_s3_tft/components/pqclean/pq_drbg_firmware.c)), which replaces PQClean's `randombytes()` during keygen. This gives deterministic, mnemonic-recoverable PQ keys — same mnemonic, same key pair. ### Firmware API The firmware exposes PQ operations via [`pq_crypto_firmware.h`](feather_s3_tft/main/pq_crypto_firmware.h): ```c /* Key generation (deterministic from mnemonic-derived seed) */ int fw_pq_ml_dsa_65_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk); int fw_pq_slh_dsa_128s_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk); int fw_pq_ml_kem_768_keygen(const uint8_t seed[32], uint8_t *pk, uint8_t *sk); /* Signing / verification */ int fw_pq_ml_dsa_65_sign(uint8_t *sig, size_t *siglen, ...); int fw_pq_slh_dsa_128s_sign(uint8_t *sig, size_t *siglen, ...); /* KEM encaps / decaps */ int fw_pq_ml_kem_768_encaps(uint8_t *ct, uint8_t *ss, const uint8_t *pk); int fw_pq_ml_kem_768_decaps(uint8_t *ss, const uint8_t *ct, const uint8_t *sk); ``` Key derivation from the mnemonic seed is via [`key_derivation.h`](feather_s3_tft/main/key_derivation.h): ```c int derive_ed25519_key(const uint8_t seed[64], uint32_t index, ...); int derive_x25519_key(const uint8_t seed[64], uint32_t index, ...); int derive_ml_dsa_65_key(const uint8_t seed[64], uint32_t index, ...); int derive_slh_dsa_128s_key(const uint8_t seed[64], uint32_t index, ...); int derive_ml_kem_768_key(const uint8_t seed[64], uint32_t index, ...); ```