Files
signer/plans/rust_firmware_unification.md

6.9 KiB

Rust Firmware Unification Plan

Status: proposal, not started. The C n_signer firmware stays in use until each Rust board matches it.

Question

Should the microcontroller firmware in ~/lt/n_signer/firmware/ be ported to this Rust project, so there is only one implementation?

Short answer

Yes, but gradually, and only after checking the risks below. Rust works well on every chip the project uses. The open questions are how much memory the post-quantum crypto needs and how much work the hardware support code takes, not whether the language fits.

Current firmware inventory (C, n_signer/firmware/)

Target MCU Stack State
feather_s3_tft ESP32-S3 ESP-IDF, TinyUSB (serial + WebUSB), TFT Working
cyd_esp32_2432s028 ESP32 (Xtensa), CH340 serial chip ESP-IDF, touch display Working
teensy41 NXP i.MX RT1062 Arduino/Teensyduino, SD card pad Working
kb2040_hidden_signer RP2040 Arduino Working
kb2040_dual_test RP2040 Arduino Test
nfc_card_signer, ble_wearable_signer, ir_airgap_signer, fpga_signer — — README only

The post-quantum code on the ESP32 boards is PQClean with an mbedtls/Keccak backend (resources/pqclean/common/crypto_backend_mbedtls.c).

Rust on microcontrollers: maturity by target

Target Rust option Notes
ESP32-S3 / ESP32 esp-idf-svc (full standard library on ESP-IDF) or esp-hal (bare metal) Backed by Espressif. Xtensa chips need the espup toolchain; RISC-V ESP32 chips (C3/C6/H2) use normal Rust.
RP2040 embassy-rp, rp2040-hal Excellent. USB CDC is supported through embassy-usb / usb-device.
Teensy 4.1 imxrt-hal, teensy4-bsp Usable, but a smaller community.
FPGA Only on a RISC-V soft core Rust does not apply to the logic itself.

How portable this codebase is

Portable logic (should build without the standard library, using alloc): dispatcher, role_table, selector, enforcement, key_store, alg_cache, pq_crypto, pq_drbg, mnemonic, auth_envelope, the cipher logic in otp_pad, and error.

Linux-only parts that would stay in the Linux binary:

Dependencies:

  • sha2, hmac, sha3, chacha20poly1305, ed25519-dalek, x25519-dalek, ml-kem, ml-dsa, slh-dsa: all can build without the standard library (turn off default features).
  • secp256k1: wraps a C library. It can run on microcontrollers but needs a C cross-compiler; consider pure-Rust k256 for firmware.
  • serde_json: can run with only alloc. Consider serde-json-core if heap is tight.
  • nostr-core (../nostr_core_lib_rust/core): currently needs the standard library (chrono, tracing, rand). It needs a no-std feature.

Risks

  1. Post-quantum memory use. RustCrypto ml-dsa 0.1 and slh-dsa (release candidate) are young, and their stack use has not been measured. This is critical on the RP2040 (264 KB of RAM). Measure before committing.
  2. Hardware support code. ESP-IDF TinyUSB composite devices (serial + WebUSB), the display drivers and touch input have to be recreated or wrapped. esp-idf-svc lessens this by keeping the ESP-IDF drivers underneath.
  3. Toolchain. Xtensa needs the espup fork of the compiler. RP2040 and i.MX RT use standard Rust.
  4. Different code on each side of the wire. Keeping separate C and Rust implementations is harder to keep in step, but it also means one bug does not hit every device. The conformance vectors (step 8) help either way.

Target architecture

flowchart TD
    A[signer-core no_std + alloc] --> B[signer-linux]
    A --> C[fw-feather-s3 esp-idf-svc]
    A --> D[fw-cyd esp-idf-svc]
    A --> E[fw-kb2040 embassy-rp]
    A --> F[fw-teensy41 imxrt-hal]
    G[C n_signer firmware] -.retired per board after parity.-> C

Platform interfaces in signer-core:

  • Rng: OS random source on Linux, hardware RNG on the microcontroller
  • SecureBuffer: mlock on Linux, a zeroized static region on the microcontroller
  • Clock: wall-clock time or a monotonic counter (policy windows, event created_at)
  • PadStorage: file on Linux, SD card or pad derived from the mnemonic on firmware
  • UserConfirm: TUI prompt on Linux, buttons or touch on firmware
  • Transport: framing of JSON-RPC over whatever byte stream the platform has (Unix socket, USB serial, UART, BLE)

Plan

  1. Decide scope. Pilot one board in Rust; the C firmware stays in use.
  2. No-std build check. A throwaway crate that depends on all the crypto crates with default-features = false, built for thumbv6m-none-eabi (RP2040) and xtensa-esp32s3-espidf / xtensa-esp32s3-none-elf.
  3. Memory profiling. Peak stack and heap for ML-DSA-65 keygen/sign and SLH-DSA-128s sign on real hardware. Compare with the C numbers from firmware/teensy41/check_stack.sh and pq_profile_results.json.
  4. Workspace split. Create a signer-core crate (no-std + alloc) holding the portable modules listed above.
  5. Platform interfaces. Define the interfaces above in signer-core, with Linux versions in signer-linux.
  6. nostr-core without the standard library. Add a std feature (on by default) and make chrono/tracing optional, or move only what signer-core needs.
  7. Linux shell. Reduce the current binary to signer-linux: sockets, mlock, threads, TUI and client, all on top of signer-core.
  8. Conformance vectors. Shared test vectors: the same mnemonic gives identical public keys and signatures in the C firmware, Rust Linux and Rust firmware. Schnorr signatures need fixed auxiliary randomness. Extend tests/pq_conformance.rs.
  9. Feather S3 TFT pilot. Use esp-idf-svc: USB CDC JSON-RPC, the TFT through mipidsi + embedded-graphics, and buttons for confirmation.
  10. Parity check. Run usb-test.html and examples/feather_*.py against the Rust firmware.
  11. Go/no-go review. Binary size, RAM headroom, signing latency, developer experience.
  12. Remaining boards (if go). CYD (esp-idf-svc), then KB2040 (embassy-rp, no standard library), then Teensy 4.1 (imxrt-hal, least mature).
  13. New targets and retirement. Start NFC/BLE/IR/FPGA-soft-core in Rust. Retire each C firmware only when its Rust version matches it.

Fallback if the go/no-go fails

Keep the C firmware for microcontrollers. Treat the JSON-RPC API (n_signer/api.md) and the conformance vectors from step 8 as the shared contract between the two codebases.