Skip to content

ml-dsa: reduce stack usage and add low-memory signing - #1457

Draft
tob-scott-a wants to merge 1 commit into
RustCrypto:masterfrom
tob-scott-a:feat/ml-dsa-reduce-stack
Draft

tob-scott-a wants to merge 1 commit into
RustCrypto:masterfrom
tob-scott-a:feat/ml-dsa-reduce-stack

Conversation

@tob-scott-a

Copy link
Copy Markdown
Contributor

ML-DSA constructs large matrix and secret-vector temporaries during key generation and signing. This reduces construction/cloning stack usage with alloc and adds an opt-in, allocation-free signing path for embedded applications.

  • With alloc, matrix rows and cached key components are allocated individually to avoid constructing or cloning a full expanded key on the stack. The default configuration retains its caches.
  • The new low-memory feature retains the public matrix seed and three raw secret vectors, regenerates matrix entries and secret NTTs one polynomial at a time, and uses in-place transforms. Key generation and public-key derivation also process matrix products one row at a time.
  • SigningWorkspace<P> provides reusable inline scratch, with const constructors for all three parameter sets. Deterministic, randomized, and precomputed-mu signing methods accept caller-owned scratch. Existing signing methods construct scratch locally when low-memory is selected. With default-features = false, signing needs no allocator; with alloc enabled, the returned signature still allocates its components.
  • With zeroize, workspace intermediates are erased after successful signing and on drop. Encodings and signature outputs are unchanged. The library continues to forbid unsafe code.

Stack and RAM measurements

Cortex-M33 results below compare upstream a840817 with this branch using low-memory, no allocation, and zeroize. Both use the same standalone probe, Rust 1.95.0, thumbv8m.main-none-eabi, optimization level 3, LTO, one codegen unit, and QEMU 11.0.3's mps2-an505 model.

Bytes: before / after ML-DSA-44 ML-DSA-65 ML-DSA-87
Retained signing key 41,120 / 12,480 65,696 / 17,600 104,608 / 23,744
Key-generation stack 176,436 / 56,400 277,452 / 82,632 347,612 / 95,836
Signing stack 70,388 / 15,416 103,868 / 18,584 132,092 / 22,640
Verification stack 55,436 / 42,088 66,828 / 45,264 75,132 / 64,060
External signing workspace (after only) 15,360 19,968 26,624

The workspace resides in static SRAM separately from the call stack. Counting it too, signing scratch uses 30,776 / 38,552 / 49,264 bytes instead of 70,388 / 103,868 / 132,092 bytes. Retained signature sizes are unchanged at 5,152 / 6,704 / 9,280 bytes.

These are written-stack watermarks for a fixed zero seed and message, including startup/caller frames and excluding static objects. They are smoke measurements, not worst-case firmware budgets. Interrupt frames, key generation, output storage, and the rest of the application require separate RAM budgeting. The emulated board uses 2 MiB of SRAM to accommodate the baseline.

A separate reused-key ML-DSA-65 host benchmark measured approximately 518 µs cached versus 877 µs with the workspace (roughly 1.7× slower), using fixed inputs, no allocation, zeroize, and 40 Criterion samples on ARM64 macOS. Regenerating the matrix and secret NTTs trades CPU time for RAM; host timings do not establish real-device cycle costs.

Reproduction instructions are in the Cortex-M33 probe README. The host stack_usage example and a 256 KiB allocation-backed lifecycle regression test are also included. CI adds all three Cortex-M33 parameter sets with a 28 KiB signing-stack limit and expands the no_std matrix to Cortex-M33.

Timing/cache review and validation

The regenerated matrix depends only on public rho and row/column indices. Dense secret products, NTTs, and norm scans use fixed public bounds and addresses. Every norm scan visits all coefficients before the existing signing rejection decisions. No coefficient-dependent early exits, sparse secret products, new division, or secret-indexed lookups were added.

Source review and disassembly checks found no new timing/cache hazards. Cortex-M33 code for all parameter sets at optimization level 3, and ML-DSA-65 at level 2, contained no division instructions or division helper calls. ARM64 and x86-64 code was also scanned for division and floating-point square-root instructions. Physical power/EM leakage and fault resistance have not been tested on hardware.

Local validation passed:

  • Byte-for-byte comparisons with the vector-based signer across all parameter sets, four seeds, and three randomizers, including workspace reuse and erasure checks.
  • Release suites with default features, all features, no default features, and allocation-free low-memory,zeroize, including ACVP, Wycheproof, serialization, and property tests.
  • Rust 1.85 library tests, release Clippy checks, formatting, and Cortex-M4/Cortex-M33/wasm32 builds.
  • Cortex-M33 key generation, signing, and verification for all parameter sets; the existing allocation-backed stack regression.

@tob-scott-a
tob-scott-a force-pushed the feat/ml-dsa-reduce-stack branch from d41a2dd to d4035e9 Compare October 8, 2026 21:35

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant