264 lines
9.9 KiB
Markdown
264 lines
9.9 KiB
Markdown
# 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
|
||
|
||
## 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)
|
||
|
||
The mbedtls backend uses:
|
||
- `mbedtls_sha256_ret()` for SHA-256 (ESP32-S3 hardware accelerated)
|
||
- `mbedtls_sha512_ret()` for SHA-512 (ESP32-S3 hardware accelerated)
|
||
- `mbedtls_md` API for SHA3-256, SHA3-512, SHAKE-128, SHAKE-256
|
||
|
||
### Enabling SHA3/SHAKE in menuconfig
|
||
|
||
mbedtls does not enable SHA3/SHAKE by default. You must enable them in
|
||
menuconfig before building:
|
||
|
||
```
|
||
Component config → mbedTLS → Hash functions → SHA-3
|
||
Component config → mbedTLS → Hash functions → SHAKE
|
||
```
|
||
|
||
Or add to `sdkconfig.defaults`:
|
||
```
|
||
CONFIG_MBEDTLS_SHA3_C=y
|
||
CONFIG_MBEDTLS_SHAKE_C=y
|
||
```
|
||
|
||
If mbedtls does not have SHAKE support, the firmware build will fail at link
|
||
time with unresolved `mbedtls_md_info_from_type(MBEDTLS_MD_SHAKE128)`. In that
|
||
case, either enable the config options above or patch mbedtls with a
|
||
Keccak/SHAKE contribution.
|
||
|
||
### 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'/<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`](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, ...);
|
||
```
|