Added remaining-todo document for Teensy 4.1 signer: captures v0.1.1-v0.1.5 fixes, current 20/24 suite score, and next steps for ml-dsa-65 sign hang (NTT correctness in matvec/scalar wrappers) and OTP decrypt mismatch

This commit is contained in:
Laan Tungir
2026-07-27 19:14:22 -04:00
parent b1f8076b1d
commit d7369646fb
+146 -408
View File
@@ -1,418 +1,156 @@
# Plan: Teensy 4.1 Signer Remaining Fixes # Teensy 4.1 Signer Remaining Fixes
This document is the handoff and remediation plan for the unresolved issues in **Status as of v0.1.5 (2026-07-27)**
the Teensy 4.1 signer after the first full hardware suite. It is based on the
current implementation under [`firmware/teensy41/signer/`](../firmware/teensy41/signer/),
the hardware suite in [`firmware/teensy41/test_signer.py`](../firmware/teensy41/test_signer.py),
and the latest reported result: **14 passing / 5 failing**.
## Current baseline ## Background
### Passing behavior The Teensy 4.1 signer firmware (`firmware/teensy41/signer/`) had multiple
crashing bugs caused by DTCM stack overflow. The Teensy 4.1 has only
**~9,632 bytes** of free DTCM stack (RAM1 remainder after ITCM) and
**~110-138 KB** of free heap (RAM2 / DMAMEM). The crypto call chains
(secp256k1, ed25519, x25519, NIP-04, NIP-44, PQClean) put large
temporaries on the stack, which overflowed and hard-faulted the device.
- USB CDC length-prefixed JSON-RPC transport is synchronized after removing ## What's fixed (v0.1.1 → v0.1.5)
runtime `Serial.print*` traffic from the framed channel.
- `get_info` works.
- secp256k1 `get_public_key`, Schnorr `sign`, `verify`, and `derive` work.
- X25519 `get_public_key` and `derive_shared_secret` work.
- SLH-DSA-128s key generation and signing work.
- ML-DSA-65 and ML-KEM-768 key generation work.
- Auto-generate and auto-approve test switches work.
- The persistent secp256k1 context prevents the prior `get_public_key` reboot.
### Remaining failures | Verb(s) | Root cause | Fix | Version |
|---|---|---|---|
| `nostr_nip04_encrypt`/`decrypt` | `secp256k1_ecmult_const` allocated two 16-entry `secp256k1_ge` tables (~3.5 KB) on the stack per ECDH call | `ECMULT_CONST_GROUP_SIZE 5→4` (tables 16→8, ~1.7 KB saved) | v0.1.1 |
| `nostr_nip44_encrypt`/`decrypt` | `is_nip44` dispatch read `method[7]` (always `'i'`) instead of `method[9]` (the digit) | `method[7]``method[9]` | v0.1.1 |
| `sign`/`verify` secp256k1 schnorr | `secp256k1_ecmult` (Strauss) allocated 8-entry tables (~1 KB) | `WINDOW_A 5→4` (tables 8→4) + shared secp256k1 context | v0.1.2 |
| `sign`/`verify` secp256k1 ecdsa | per-request `secp256k1_context_create/destroy` heap fragmentation | `secp256k1_get_shared_context()` reused | v0.1.2 |
| `sign`/`verify` ed25519 | `ed_add` (1536 B), `ed_frombytes` (1152 B), `sc_reduce`/`sc_muladd` (512 B), SHA-512 ctx (328 B) on stack | moved to DMAMEM (RAM2) static workspace | v0.1.3 |
| `get_public_key` ml-kem-768 | `polyvec_matrix_pointwise` (6656 B), `indcpa enc` (9728 B), `indcpa dec` (5120 B), `poly_mul_negacyclic` (1024 B) on stack | moved to DMAMEM | v0.1.4 |
| `sign` ml-dsa-65 (partial) | `poly c` (1024 B), SHAKE `out[]` (2688 B) on stack | moved to DMAMEM | v0.1.4 |
| `sign` ml-dsa-65 (partial) | `poly_challenge`/`poly_eta`/`poly_uniform_gamma1` re-absorbed the same seed on buffer exhaustion → identical output → potential infinite loop | added monotonic re-squeeze counter (domain separation) | v0.1.5 |
| Priority | Failure | Current symptom | ### Verified on hardware
|---|---|---|
| P0 | Ed25519 implementation | Public key and signature are all zero; verify fails |
| P0 | OTP initialization and round-trip semantics | `encrypt` reports `otp pad not initialized`; the current single monotonic offset also cannot decrypt a just-produced ciphertext correctly without explicit offset handling |
| P1 | ML-DSA-65 performance | Signing exceeds the hardware-test timeout |
| P1 | ML-KEM-768 performance | Encapsulation exceeds the hardware-test timeout |
| P1 | Build reproducibility / memory headroom | The successful build relies on a custom linker command and leaves very little RAM1/stack headroom |
| P1 | Hardware-suite robustness | Current serial timeout logic can hang indefinitely after a timeout and the suite does not yet verify every returned signature on host/device |
| P2 | Debug configuration hygiene | `DEBUG_AUTO_GENERATE` and `DEBUG_AUTO_APPROVE` are enabled in source and need an explicit test/production configuration path |
| P2 | Documentation drift | [`README_crypto.md`](../firmware/teensy41/signer/src/README_crypto.md) still describes old filenames and an obsolete simple build command |
## Fix order and rationale `python3 firmware/teensy41/test_classical.py --port /dev/ttyACM0`
**16 passed, 0 failed** in one uninterrupted boot:
```mermaid ```
flowchart TD get_info ✅ secp256k1 pubkey ✅ ed25519 pubkey ✅ x25519 pubkey ✅
B[Freeze reproducible baseline] --> E[Fix Ed25519 correctness] schnorr sign ✅ schnorr verify ✅ ecdsa sign ✅ ecdsa verify ✅
B --> O[Fix OTP initialization and offset contract] ed25519 sign ✅ ed25519 verify ✅ x25519 shared secret ✅ derive ✅
E --> P[Replace PQ schoolbook multiplication] nostr_get_public_key ✅ nostr_sign_event ✅ nip04 round-trip ✅ nip44 round-trip ✅
O --> P
P --> M[Re-balance RAM and linker placement]
M --> T[Run full deterministic hardware suite]
T --> R[Restore production security defaults]
R --> D[Update documentation and release gate]
``` ```
Correctness fixes come before performance work. PQ optimization must preserve `python3 firmware/teensy41/test_signer.py`**20/24** (all classical + Nostr +
known-answer behavior, and memory placement must be revisited after the hot ml-kem-768 keygen + ml-dsa-65 keygen + slh-dsa-128s keygen+sign + OTP
paths are changed. encrypt pass; 4 fail: OTP decrypt, ml-dsa-65 sign, encapsulate/decapsulate
ml-kem-768).
---
## What's still broken
## P0 — Establish a reproducible baseline
### 1. `sign` ml-dsa-65 — hangs (rejection loop never accepts)
### Problem
**Symptom:** `sign` with `{"algorithm":"ml-dsa-65","index":0}` hangs — no
The current successful build uses the custom linker script response within 180 seconds. The device stays alive (main loop responsive,
[`imxrt1062_t41_flashmem.ld`](../firmware/teensy41/signer/imxrt1062_t41_flashmem.ld), `get_info` works after). No `CrashReport` (not a hard fault).
but the normal Arduino command does not select it automatically. The old
[`build_opt.h`](../firmware/teensy41/signer/build_opt.h) referenced in prior **What's been ruled out:**
notes is not present. The LVGL configuration was also copied outside the - Stack overflow: FIXED (v0.1.4). No more CFSR=0x82 hard fault.
repository into the user's Arduino library directory. - SHAKE re-squeeze infinite loop: FIXED (v0.1.5). Re-squeeze now uses a
monotonic counter for domain separation.
### Actions - NTT core correctness: VERIFIED. The host-side test
(`firmware/teensy41/signer/tests/host_test_ntt.c`) passes all 4 tests:
- [ ] Add a repository-owned build/flash script, for example round-trip, mul-vs-schoolbook, poly wrappers, known products.
`firmware/teensy41/build_signer.sh`, that:
- copies [`lv_conf.h`](../firmware/teensy41/signer/lv_conf.h) to the location **What's left:**
required by the installed LVGL Arduino library, or injects an explicit The 1000-iteration rejection loop in
`LV_CONF_PATH` build define; [`mldsa65_sign.c::crypto_sign()`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c:434)
- compiles with the absolute custom linker-script path; never accepts a candidate. Every iteration fails one of the norm checks
- prints the memory report; (`z_reject`, `r0_reject`, `ct0_reject`, `hint_count > OMEGA`). Since the
- optionally uploads to the discovered Teensy port. NTT core is correct, the divergence is in the **full sign path** — most
- [ ] Add a clean diagnostic build command that disables auto-generate and likely the `matvec_mul_KL` / `scalar_mul_L` / `scalar_mul_K` wrappers in
auto-approve without editing source. [`mldsa65_sign.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c:199)
- [ ] Record the exact Arduino CLI, Teensy core, LVGL, and compiler versions. or the decomposition/rejection checks themselves.
- [ ] Preserve a hardware test log containing firmware git hash, generated npub,
build memory report, and per-verb timing. **Next step:**
1. Build a host-side full `crypto_sign` path with a fixed seed (deterministic
### Gate DRBG so both host and Teensy produce the same `rho`/`key`/`tr`).
2. Add instrumentation to dump intermediate polynomials (`y`, `w`, `w1`, `c`,
A clean checkout can build the same firmware with one documented command, and `z`, `r0`) at each rejection iteration on both sides.
that command reports non-negative free RAM1 and RAM2 without manual files 3. Compare to find the first divergence.
outside the repo. 4. The most likely culprits:
- `matvec_mul_KL`: does `poly_ntt` + `poly_pointwise_invmontgomery` +
--- `poly_invntt_tomont` produce the correct `w = A * y`?
- `scalar_mul_L` / `scalar_mul_K`: do they produce the correct
## P0 — Fix Ed25519 correctness `c * s1` / `c * s2` / `c * t0`?
- The decomposition (`w1`/`w0` split) or the rejection bounds
### Evidence and likely root cause (`GAMMA1 - BETA`, `GAMMA2 - BETA`, `OMEGA`).
- SLIP-0010 derivation is likely correct because X25519 works through the same ### 2. `decrypt` OTP — plaintext mismatch
derivation framework in [`key_derivation.cpp`](../firmware/teensy41/signer/src/key_derivation.cpp).
- Ed25519 fails specifically in [`ed25519_publickey()`](../firmware/teensy41/signer/src/ed25519.c:890) **Symptom:** `decrypt` with `{"algorithm":"otp"}` returns a result but the
and signing returns zero values. decrypted plaintext does not match the original. `encrypt` works (produces a
- The current code is described as TweetNaCl-derived, but its Edwards arithmetic ciphertext + pad offset).
is custom. In particular, [`ed_double()`](../firmware/teensy41/signer/src/ed25519.c:357)
implements doubling by calling the custom complete-addition function with the **Likely cause:** The OTP pad offset advances differently on encrypt vs
same point. This needs validation against the original implementation. decrypt, or the pad derivation from the seed produces a different offset
- The current verify equation in [`ed25519_verify()`](../firmware/teensy41/signer/src/ed25519.c:945) after the encrypt call. This is a pre-existing bug in
also needs comparison to RFC 8032; a sign convention error could remain even [`otp_pad.cpp`](firmware/teensy41/signer/src/otp_pad.cpp) /
after public-key generation is repaired. [`otp_pad.h`](firmware/teensy41/signer/src/otp_pad.h), unrelated to the
PQ/stack work.
### Actions
**Next step:** Compare the pad offset before and after `encrypt`, and verify
- [ ] Create a host-buildable Ed25519 KAT target using the same source file and `decrypt` uses the same offset. The OTP pad is XOR-based, so a mismatch means
RFC 8032 vectors: the offset is wrong or the pad bytes differ.
- empty-message vector;
- one-byte `0x72` vector; ## Build memory (v0.1.5)
- verify valid signatures and reject a one-bit mutation.
- [ ] Add a diagnostic API or temporary unit target that separately tests: ```
- SHA-512 of `abc`; RAM1: variables:154208, code:339848, padding:20600 free for local variables:9632
- scalar clamping; RAM2: variables:413600 free for malloc/new:110688
- base-point encoding; ```
- `1 * B`, `2 * B`, and a known secret scalar;
- `ed25519_publickey()` against RFC 8032. ~110 KB of free heap remains. All large crypto temporaries (secp256k1,
- [ ] Compare [`ed_add()`](../firmware/teensy41/signer/src/ed25519.c:327), ed25519, x25519, NIP-04, NIP-44, ml-kem-768, ml-dsa-65 keygen/sign) are
[`ed_double()`](../firmware/teensy41/signer/src/ed25519.c:357), now in DMAMEM (RAM2). The only remaining stack pressure is the ml-dsa-65 sign
[`ed_scalarmult()`](../firmware/teensy41/signer/src/ed25519.c:365), and NTT path, which is an algorithmic correctness issue, not a memory issue.
[`ed_frombytes()`](../firmware/teensy41/signer/src/ed25519.c:410) line by
line against a known-good audited source. ## Test commands
- [ ] Prefer replacement over repairing ad hoc formulas if practical. Candidate
strategies: ```bash
- vendor the original TweetNaCl sign implementation unchanged and expose thin # Build + flash
wrappers; bash firmware/teensy41/build_signer.sh --flash
- use a compact audited Ed25519 implementation already present in a trusted
dependency; # Classical + Nostr suite (16 tests, all pass)
- keep the existing working X25519 implementation separate. python3 firmware/teensy41/test_classical.py --port /dev/ttyACM0
- [ ] Ensure secret and expanded-key buffers are zeroized on all paths.
- [ ] Add deterministic host and Teensy tests for derive → public key → sign → # NIP-04 + NIP-44 round-trip
verify using a fixed mnemonic. python3 firmware/teensy41/test_nip04.py --port /dev/ttyACM0
### Gate # Full suite (24 tests, 20 pass)
python3 firmware/teensy41/test_signer.py --port /dev/ttyACM0
- `get_public_key(ed25519)` is nonzero and equals a known host result for the
fixed test mnemonic/index. # NTT host-side correctness test
- RFC 8032 vectors pass. cc -O2 -Wall -Wextra \
- Device `sign(ed25519)` verifies both on device and with an independent host -I firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65 \
library. -I firmware/teensy41/signer/src/pqclean/common \
- A corrupted signature is rejected. -D HOST_TEST -o host_test_ntt \
firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_ntt.c \
--- firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c \
firmware/teensy41/signer/tests/host_test_ntt.c \
## P0 — Fix OTP initialization and round-trip semantics firmware/teensy41/signer/tests/host_test_ntt_stubs.c -lm
./host_test_ntt
### Initialization bug ```
[`otp_pad_init()`](../firmware/teensy41/signer/src/otp_pad.cpp:25) exists but is ## Files changed (v0.1.1 → v0.1.5)
not called after seed derivation in [`apply_mnemonic()`](../firmware/teensy41/signer/signer.ino:260).
This causes [`otp_pad_ready()`](../firmware/teensy41/signer/src/otp_pad.cpp:74) - [`firmware/teensy41/signer/src/secp256k1/src/secp256k1_arduino_config.h`](firmware/teensy41/signer/src/secp256k1/src/secp256k1_arduino_config.h) — `ECMULT_CONST_GROUP_SIZE 4`, `WINDOW_A 4`
to fail in the dispatch path. - [`firmware/teensy41/signer/src/secp256k1/src/ecmult_const_impl.h`](firmware/teensy41/signer/src/secp256k1/src/ecmult_const_impl.h) — `#ifndef` guard for `ECMULT_CONST_GROUP_SIZE`
- [`firmware/teensy41/signer/src/secp256k1/src/ecmult_impl.h`](firmware/teensy41/signer/src/secp256k1/src/ecmult_impl.h) — `#ifndef` guard for `WINDOW_A`
### Deeper contract problem - [`firmware/teensy41/signer/src/key_derivation.h`](firmware/teensy41/signer/src/key_derivation.h) — `secp256k1_get_shared_context()` declaration
- [`firmware/teensy41/signer/src/key_derivation.cpp`](firmware/teensy41/signer/src/key_derivation.cpp) — `secp256k1_get_shared_context()` definition
The current [`otp_pad_apply()`](../firmware/teensy41/signer/src/otp_pad.cpp:53) - [`firmware/teensy41/signer/src/dispatch.cpp`](firmware/teensy41/signer/src/dispatch.cpp) — `is_nip44` fix, shared context, crash diagnostics (`stamp_op`)
always consumes the current global offset. If encrypt advances from offset 0 to - [`firmware/teensy41/signer/src/ed25519.c`](firmware/teensy41/signer/src/ed25519.c) — `ed_add`/`ed_frombytes`/`sc_reduce`/`sc_muladd`/SHA-512 ctx moved to DMAMEM
N, an immediate decrypt will consume bytes N..2N rather than bytes 0..N. A - [`firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c`](firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c) — NTT working polys + `buf[4096]` to DMAMEM
round-trip therefore requires the ciphertext to carry the start offset and the - [`firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c`](firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c) — enc/dec NTT polys to DMAMEM
decrypt operation to apply the pad at that explicit offset without reusing or - [`firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c) — SHAKE `out[]` to DMAMEM, re-squeeze domain separation
silently advancing the wrong cursor. - [`firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c`](firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c) — `poly c` to DMAMEM, rejection-loop counter
- [`firmware/teensy41/signer/signer.ino`](firmware/teensy41/signer/signer.ino) — crash diagnostics (`g_last_op`, `g_mldsa65_reject_count`)
### Actions - [`firmware/teensy41/test_classical.py`](firmware/teensy41/test_classical.py) — classical + Nostr hardware test
- [`firmware/teensy41/test_nip04.py`](firmware/teensy41/test_nip04.py) — NIP-04 + NIP-44 hardware test
- [ ] Call `otp_pad_init(g_seed, g_seed_len)` in - [`firmware/teensy41/test_signer.py`](firmware/teensy41/test_signer.py) — full suite (reordered: classical+Nostr first, PQ last)
[`apply_mnemonic()`](../firmware/teensy41/signer/signer.ino:260) after seed
derivation succeeds and before `g_signer_ready` is set.
- [ ] On failure, wipe derived signer state and abort startup.
- [ ] Add `otp_pad_apply_at(buf, len, offset)` and retain a separate monotonic
allocation cursor for encryption.
- [ ] Define the wire contract explicitly:
- encrypt response returns ciphertext and the **start** `pad_offset`;
- decrypt request must include `pad_offset`;
- decrypt applies the exact pad region and does not allocate a new region;
- reject missing, malformed, out-of-range, or already-forbidden offsets.
- [ ] Decide whether decrypting previously emitted ciphertext on the same device
is an allowed diagnostic operation or whether OTP reuse policy forbids it
in production. If diagnostics allow it, guard that mode explicitly.
- [ ] Until the SDXC design lands, label the HKDF stream as a deterministic test
pad rather than a true one-time pad.
- [ ] Add power-cycle and exhaustion tests.
### Gate
- `otp_pad_ready()` is true after boot.
- Encrypt returns the start offset.
- Decrypt with that offset reproduces the plaintext.
- Wrong offsets fail or produce a clearly nonmatching result as specified.
- Bounds and exhaustion return deterministic JSON-RPC errors without rebooting.
---
## P1 — Replace ML-DSA-65 schoolbook multiplication
### Evidence
[`mldsa65_sign.c`](../firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_sign.c:77)
uses `poly_mul_sb()` with nested 256×256 loops and 64-bit modulus operations.
Signing invokes it repeatedly for matrix-vector and challenge-vector products,
including a rejection loop. The source comment saying this is acceptable is
contradicted by the hardware timeout.
A nominal NTT implementation already exists in
[`mldsa65_ntt.c`](../firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_ntt.c)
and pointwise primitives exist in
[`mldsa65_poly.c`](../firmware/teensy41/signer/src/pqclean/crypto_sign/ml-dsa-65/mldsa65_poly.c:57),
but the top-level implementation bypasses them.
### Actions
- [ ] Do not merely move `poly_mul_sb()` to ITCM as the final fix; that may
improve speed but preserves the wrong complexity and consumes scarce
RAM1.
- [ ] Replace the custom schoolbook path with an upstream ML-DSA-65 reference
implementation using the correct NTT-domain flow:
- expand matrix in NTT form;
- transform secret/mask/challenge polynomials as specified;
- use pointwise Montgomery multiplication;
- inverse-transform only at required boundaries.
- [ ] Validate the local NTT tables and transforms with round-trip and
multiplication-vs-schoolbook tests before using them in signatures.
- [ ] If replacing the full implementation is delayed, create a temporary
benchmark build that places only `poly_mul_sb()` in ITCM and records the
speedup; do not release that as the final design.
- [ ] Preserve deterministic key derivation from the mnemonic DRBG.
- [ ] Add keygen/sign/verify KATs and mutation rejection tests.
### Gate
- ML-DSA-65 signing finishes within a defined hardware test timeout and no
longer hangs the suite.
- The device-generated signature verifies independently on the host.
- The device rejects a mutated signature.
- Repeated operations do not corrupt DMAMEM work buffers.
---
## P1 — Replace ML-KEM-768 schoolbook multiplication
### Evidence
[`ml_kem_768_poly_mul_negacyclic()`](../firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/mlkem768_poly.c:86)
uses O(n²) schoolbook multiplication. Encapsulation and decapsulation call it
many times through [`indcpa.c`](../firmware/teensy41/signer/src/pqclean/crypto_kem/ml-kem-768/indcpa.c:167).
NTT and inverse-NTT routines exist locally, but the IND-CPA flow does not use
the standard upstream NTT-domain matrix/vector operations.
### Actions
- [ ] Replace the custom multiplication path with the upstream ML-KEM-768
reference flow and constants, not an improvised cyclic-NTT twist.
- [ ] Keep matrix and secret vectors in NTT representation where specified.
- [ ] Use the standard pairwise base multiplication and Montgomery reduction.
- [ ] Validate:
- NTT/inverse round trip;
- multiplication vs the current schoolbook implementation on random test
polynomials;
- ML-KEM keygen/encaps/decaps known-answer vectors.
- [ ] Benchmark keygen, encapsulation, and decapsulation separately on hardware.
- [ ] Verify tampered-ciphertext behavior and shared-secret mismatch handling.
### Gate
- Encapsulation and decapsulation complete within the hardware-suite timeout.
- Encapsulation and decapsulation produce identical shared secrets.
- Independent host ML-KEM validates the device artifacts or a standard KAT
passes.
- Tampered ciphertext does not yield the original shared secret.
---
## P1 — Memory and linker hardening
### Problem
The current linker placement was introduced to recover stack space, but the
last report still left limited RAM1 headroom. Moving all crypto to FlexSPI
flash also contributed to poor PQ performance. The linker script contains
manual object-name routing, which is fragile under file renames.
### Actions
- [ ] After Ed25519 and PQ replacements, remeasure FLASH, ITCM, DTCM, RAM2,
heap, and minimum stack watermark.
- [ ] Keep large immutable tables in flash.
- [ ] Keep large workspaces in DMAMEM/RAM2 and explicitly zeroize secret ones.
- [ ] Put only measured hot, compact routines in ITCM.
- [ ] Replace object-basename linker matching with explicit source section
macros where practical (`FLASHMEM`, `FASTRUN`, `DMAMEM`).
- [ ] Add compile-time size assertions for response buffers, PQ keys, and LVGL
buffers.
- [ ] Add runtime guards/canaries or a stack watermark diagnostic build.
- [ ] Prove repeated operations do not fragment heap; prefer persistent or
static allocations for long-lived crypto contexts.
### Gate
- No negative or marginal memory report.
- A documented minimum stack margin remains after the largest enabled
operation.
- Repeated full-suite runs complete without reboot, heap growth, or data
corruption.
---
## P1 — Strengthen the hardware suite
### Problems in current test harness
- [`send_request()`](../firmware/teensy41/test_signer.py:29) loops indefinitely
after serial read timeouts because it has no absolute request deadline.
- Ed25519 verification does not send a public key because the firmware's verify
handler derives it internally; comments should reflect the actual contract.
- ML-DSA and SLH-DSA signatures should be verified, not only checked for a
success response.
- Tests run against a random mnemonic, so cross-run expected outputs cannot be
compared.
- Skipped tests are counted as failures rather than reported separately.
### Actions
- [ ] Add an absolute per-request deadline and classify timeout vs disconnect vs
invalid frame.
- [ ] Add `--quick`, `--full`, and per-algorithm timeout profiles.
- [ ] Add a deterministic test seed/mnemonic build mode. Keep random mode as a
separate smoke test.
- [ ] Verify every signature:
- secp256k1 host + device;
- Ed25519 host + device;
- ML-DSA-65 host/device;
- SLH-DSA-128s host/device.
- [ ] Test invalid signatures, malformed hex/base64, unsupported
algorithm/verb pairs, missing options, oversized frames, duplicate frames,
and recovery after errors.
- [ ] Test indices 0 and 1 and compare repeated deterministic derivations.
- [ ] Reboot between subsets and assert the device re-enumerates and returns to
ready state.
- [ ] Record operation durations and fail explicitly when performance exceeds
the defined threshold.
### Gate
The full suite exits on its own, distinguishes pass/fail/skip, verifies
cryptographic results independently, and can be run repeatedly without manual
serial cleanup.
---
## P2 — Debug and production profiles
### Actions
- [ ] Define build-time profiles rather than editing source:
- `test`: fixed or random auto-generated mnemonic, optional auto-approve;
- `production`: interactive mnemonic entry, approvals required, auth policy
explicit, debug serial disabled after transport starts.
- [ ] Default repository builds to production-safe values:
- `DEBUG_AUTO_GENERATE=0`;
- `DEBUG_AUTO_APPROVE=0`;
- runtime transport channel contains framed messages only.
- [ ] Ensure no mnemonic, seed, private key, or signature secret is logged.
- [ ] Add a conspicuous on-screen label when a test build has auto-approval.
### Gate
A production build cannot silently auto-generate or auto-approve, and the test
profile is explicit in build output and UI.
---
## P2 — Documentation updates
### Actions
- [ ] Update [`README_crypto.md`](../firmware/teensy41/signer/src/README_crypto.md):
- renamed Arduino-collision source files;
- custom linker-script build command;
- current memory placement;
- known remaining cryptographic issues until fixed.
- [ ] Update [`firmware/teensy41/README.md`](../firmware/teensy41/README.md)
with production/test build commands and latest suite status.
- [ ] Link this plan from the implementation and bring-up documents.
- [ ] Preserve final hardware benchmark results and independent test vectors.
### Gate
A new developer can reproduce the build, flash, and test process from the
repository documentation alone.
---
## Recommended implementation sequence
1. Build reproducibility script + deterministic test profile.
2. OTP initialization and explicit offset contract.
3. Replace/fix Ed25519 and pass RFC 8032 vectors.
4. Replace ML-KEM schoolbook multiplication and pass KAT + round trip.
5. Replace ML-DSA schoolbook multiplication and pass KAT + sign/verify.
6. Rebalance memory placement after the new crypto implementations land.
7. Expand and stabilize the hardware suite.
8. Restore production defaults and update documentation.
## Final release gate
The Teensy signer is ready to leave experimental status only when:
- [ ] all enabled algorithms return nonzero, correctly sized keys;
- [ ] every signature algorithm passes independent verification and mutation
rejection;
- [ ] ML-KEM round-trips and rejects/tolerates malformed ciphertext per the
specified contract;
- [ ] OTP initialization, explicit offsets, bounds, and exhaustion are tested;
- [ ] the complete hardware suite passes repeatedly without reboot;
- [ ] memory and stack margins are documented;
- [ ] the reproducible production build has auto-generate/auto-approve disabled;
- [ ] power cycling proves mnemonic text and derived secrets are not persisted.