Files
n_signer/firmware/README.md

285 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**: 530 seconds
- **Signing**: 530 seconds
- **Verification**: 0.52 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, ...);
```