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:
- Enter bootloader mode: hold BOOT, tap RESET, release BOOT.
- Confirm bootloader USB device appears (typically
/dev/ttyACM0). - Flash:
cd firmware/feather_s3_tft
idf.py -p /dev/ttyACM0 flash
- Exit bootloader and run app: ensure BOOT is released, then tap RESET once.
- Confirm app USB identity:
lsusb | grep -i 303a
# expect: 303a:4001 n_signer Feather S3 TFT n_signer
Quick validation
CDC path:
./.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 - 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:
- Open
examples/cyd_webserial_demo.htmlin 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_nip44encrypt+decrypt, andencrypt/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 §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:
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
lsusbshows303a: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, not303a:1001. - If no
/dev/ttyACM*appears, unplug/replug after reset and re-checklsusb. - 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)
with two implementations:
resources/pqclean/common/crypto_backend_openssl.c— host build (OpenSSL EVP)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.cbecause ESP-IDF v5.x mbedtls does not expose SHAKE (and SHA3 is only available whenCONFIG_MBEDTLS_SHA3_Cis set) through thembedtls_mdAPI. 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/ 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'/<n>'/0/0 |
NIP-06, existing |
| ed25519 (SSH) | m/44'/102001'/<n>'/0'/0' |
SLIP-0010 |
| x25519 (age) | m/44'/102002'/<n>'/0'/0' |
SLIP-0010 |
| ML-DSA-65 | m/44'/102003'/<n>'/0'/0' |
seed → PQClean keygen |
| SLH-DSA-128s | m/44'/102004'/<n>'/0'/0' |
seed → PQClean keygen |
| ML-KEM-768 | m/44'/102005'/<n>'/0'/0' |
seed → PQClean keygen |
The PQ derivation produces a 32-byte seed that feeds a deterministic
SHAKE-256 DRBG (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:
/* 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:
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, ...);