The ml-dsa feature supplies original Rust implementations of ML-DSA-44, ML-DSA-65, and ML-DSA-87.
It enables sha3.
It does not need alloc, std, operating-system entropy, or an external implementation.
signatures, auth, and full include it.
RustCrypto ML-DSA 0.1.1 is pinned only as a development oracle.
The portable implementation and the local baseline are complete. Target qualification continues for macOS AArch64, Linux x86-64, AArch64, IBM Z, and POWER, and the RISC-V kernels. The evidence and limits below do not claim universal performance leadership or constant time for whole operations.
The arithmetic and encodings come from FIPS 204, final, 2024-08-13. The potential corrections, updated 2026-07-31, were checked with the NIST FAQ on 2026-10-16 UTC. NIST calls them potential corrections, not an amended final publication.
| Artifact | SHA-256 |
|---|---|
| Final FIPS 204 PDF | 57239b9f84c03227eda3ca0991204dc7764c79af9ce2e6824eda774918d46b6b |
| Potential corrections spreadsheet | 5bc93ce63bc647e6d1d456cb2d3a171426c15aca4a7a0e0edd40d08b7a34c793 |
- The signing cap is 821 attempts. This includes the correction from 814.
- Challenge hashing uses
mu || w1. - Hint subtraction wraps to the high-part modulus minus one.
- The portable implementation uses its own unsigned Montgomery representation, with multiplication operands below 2q. It does not implement the spreadsheet's signed-representative pseudocode. The multiplication routine carries the proof of its reduction bound. Inside their transforms, AVX2 and AVX-512 use signed Montgomery multiplication and NEON uses signed Barrett multiplication. Both return the same canonical residues.
- The roots come from the standard's root 1753 and eight-bit index reversal. They are not imported from another implementation's table.
The 2026-10-16 qualification summary links the standards reconciliation, which maps all twelve spreadsheet rows to the selected source or identifies them as editorial. The local report retains fetched sources, checksums, regression references, ACVP coverage, and the limited formal-proof inventory. It also records the final-source native/portable AddressSanitizer tests and guarded signing-stack residue checks on macOS AArch64. Those bounded checks cover the selected calls and artifacts; broader target qualification remains separate. They do not establish module validation or whole-operation constant time.
The ACVP manifest describes all 615 retained ACVP cases and their provenance. The Wycheproof manifest describes all 1,138 pinned Wycheproof signing and verification cases. No third-party implementation source is copied, translated, or bundled into the primitive.
Each parameter set has its own types for the public key, secret key, seed, signature,
prepared secret key, and prepared public key, and a storage type for each prepared key.
A prepared key is generic over where its storage lives:
&mut storage that the caller owns, from prepare,
or a Box that prepare_in allocates with alloc.
The encodings have these sizes, in bytes. The first three columns are raw FIPS 204 encodings.
The others are RFC 9881 DER: the public key's SubjectPublicKeyInfo,
and version 1 PKCS #8 private keys in the seed and expanded forms.
The seed itself is 32 bytes for every parameter set.
| Parameter set | Public key | Expanded secret key | Signature | Public-key SPKI | Seed PKCS #8 | Expanded PKCS #8 |
|---|---|---|---|---|---|---|
| ML-DSA-44 | 1312 | 2560 | 2420 | 1334 | 54 | 2588 |
| ML-DSA-65 | 1952 | 4032 | 3309 | 1974 | 54 | 4060 |
| ML-DSA-87 | 2592 | 4896 | 4627 | 2614 | 54 | 4924 |
keypair_from_seedborrows a 32-byte seed. The secret key does not keep a copy of the seed. To keep it, use the parameter set's seed owner, such asMlDsa44Seed: it is notCloneorCopy, clears the seed on drop, and expands it withkeypairorkeypair_in.- Expanded import checks the noise coefficient ranges, rebuilds the public key, and verifies the redundant low polynomial and the public-key hash.
- The expanded format has no independent consistency check for the signing seed K.
- Public-key decoding accepts every bit pattern of the correct length.
from_spki_derandto_spki_deruse the RFC 9881 SubjectPublicKeyInfo: this parameter set'sid-ml-dsa-*identifier with absent parameters, then the raw public key in a BIT STRING. Import accepts only that unique DER encoding. It reports another algorithm or parameter set, including HashML-DSA, asUnsupportedAlgorithm. Neither direction allocates.SecretKey::from_pkcs8_derimports an RFC 9881 private key from an RFC 5958 OneAsymmetricKey (PKCS #8). It accepts the seed, expanded, and both forms, selected by tag. The container is version 1, or version 2 with a public key. Attributes are rejected asUnsupportedEncoding. The algorithm identifier must be this parameter set's, with absent parameters. DER must be canonical and complete. An expanded key gets the expanded-import checks. A seed is expanded and not retained. In the both form, the expanded key must equal the one the seed generates (RFC 9881 section 8.2), compared without an early exit. A version 2 public key must belong to the key. A disagreement isInvalidSecretKey.Seed::from_pkcs8_deraccepts the seed and both forms with the same checks. An expanded-only key isUnsupportedEncoding: expansion cannot recover a discarded seed.Seed::to_pkcs8_der_intowrites the RFC 9881 recommended seed form.SecretKey::to_pkcs8_der_intowrites the expanded form. Both write version 1 without a public key into a fixed-size buffer that the caller owns and clears. Neither direction allocates.- The RFC 9881 Appendix C examples, RustCrypto ML-DSA 0.1.1, and aws-lc-rs 1.18.2 anchor both directions. aws-lc-rs imports both exported forms. RustCrypto supports only the seed form.
- Key import returns
MlDsaKeyError. Generation, preparation, and signing returnMlDsaError. - Signing with a secret key and preparing it re-check its noise coefficient ranges.
Construction validates every key, so
MlDsaError::InvalidSecretKeyarises only if key memory changes after validation. The operation clears its secret state before it returns.
- A signature needs canonical hints, zero unused hint bytes, and an admissible response norm.
- Structural parsing is separate from authentication.
- Verification returns the opaque
VerificationError, also for a context that is too long. - A context is a byte string of at most 255 bytes.
sign_deterministicuses the standard's all-zero randomness.sign_withasks the caller for exactly 32 bytes of cryptographic randomness.try_signandtry_generate_keypairuse operating-system entropy, and only withgetrandom.- Context validation happens before entropy acquisition.
- If a callback fails, no signature or key is published, and existing keys stay usable.
- Callback buffers have zeroizing owners before the callback starts.
- A successful callback must fill every byte.
MlDsaPrehash binds a borrowed digest to its algorithm and checks the digest length.
The caller computes the digest over the original message.
All twelve FIPS 204 hash identifiers are supported: SHA-224/256/384/512, SHA-512/224, SHA-512/256,
SHA3-224/256/384/512, SHAKE128 with 32 output bytes, and SHAKE256 with 64 output bytes.
The signature authenticates the prehash algorithm identifier, the digest, the context,
and the pure or prehash domain.
The collision strength of the hash can limit the security of the parameter set. Digests of 224, 256, 384, and 512 bits supply at most 112, 128, 192, and 256 bits. The supported SHAKE outputs supply 128 and 256 bits.
- The
Verifiertrait uses pure ML-DSA with an empty context. - Signing methods keep randomness and context explicit.
No implicit
Signermode exists. - The public API does not accept an unbound external message representative. The private ACVP harness tests the standard's internal interface.
- Public keys and signatures support
serde. Secret serialization also needsserde-secrets. - Deserialization uses the same strict import checks, rejects extra bytes, and guards partial reads of secret sequences.
- Secret owners are not
CloneorCopy, redactDebug, and export explicitly intoSecretBytes. - A prepared key reads only its storage, not its source key.
Secret storage holds the transformed secret polynomials, the public matrix,
and the first 128 bytes of the encoded key: rho,
K, and tr. - A prepared secret key clears the transformed polynomials and
Kon drop. A failed preparation clears them before it returns. The secret storage clears them again on drop. - A prepared key in borrowed storage borrows that storage.
A prepared key from
prepare_inowns its box; it isSend,Sync, and'staticwhen its allocator is, so a long-lived signer or verifier can keep and share it.
Operations allocate no heap memory, except the explicit *_in constructors below.
They do not copy messages into an intermediate buffer of message size.
Pure signing absorbs the borrowed message.
Prehash callers can hash incrementally before signing.
The compact path expands matrix rows when it needs them.
For repeated work, prepare(&mut storage) fills storage that the caller owns with the complete matrix
and the transformed key.
It does not change signature bytes or validation.
The caller selects where the storage lives: stack, static, or heap.
new() is a const fn, so a static initializer can also hold the storage.
The module's Rustdoc example shows the flow.
Preparation writes the storage in place and returns a prepared key that borrows the storage.
The storage can be used again after the prepared key drops.
With alloc, prepare_in(alloc) allocates the storage from a caller-selected Allocator
and prepares it in place.
The returned prepared key owns the box and does not borrow the source key.
On x86-64, AArch64, RISC-V, POWER, and IBM Z Linux release builds,
a non-inlined call that prepares a secret and a public key with prepare_in
has a frame of 1,128 to 2,240 bytes for every parameter set,
so the storage is not built on the stack first (2026-10-13, nightly-2026-09-30).
That is a frame measurement, not a whole-call bound; the table below does not include prepare_in.
| Parameter set | Prepared secret-key storage | Prepared public-key storage |
|---|---|---|
| ML-DSA-44 | 28,800 B | 20,544 B |
| ML-DSA-65 | 48,256 B | 36,928 B |
| ML-DSA-87 | 81,024 B | 65,600 B |
Measured stack bounds for a whole call, in bytes, without the caller's own storage:
| Operation | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| Key generation | 24,976 | 34,688 | 42,000 |
| Expanded secret-key import | 27,024 | 36,384 | 45,536 |
| Compact signing (pure, hedged, prehash) | 37,488 | 48,480 | 63,408 |
| Prepared signing | 25,088 | 30,960 | 39,744 |
| Compact verification (pure, prehash) | 12,496 | 13,504 | 15,568 |
| Prepared verification | 12,304 | 13,312 | 15,376 |
Secret-key prepare |
1,504 | 1,504 | 1,552 |
Public-key prepare |
4,000 | 4,000 | 4,000 |
Storage new |
320 | 320 | 320 |
The rows for secret-key prepare, prepared signing and verification, and storage new
were measured before the 2026-10-13 change.
That change copies rho, K, and tr into secret storage,
makes prepared keys read only their storage, and builds storage from one zero constant.
Those rows have not been measured again.
The later signing, verification, matrix-expansion, and vector-kernel changes
(6cd54edd and 4d76f163, 2026-10-20 and 2026-10-21) can change every row.
No row has been measured since them.
How these values were measured:
- Each value is the largest static bound across native and
portable-onlyrelease builds (fat LTO, one codegen unit), built with the pinned nightly compiler, for: s390x, POWER, RV64, x86-64, and AArch64 Linux GNU; and RV32, Thumb, x86-64, and AArch64 bare metal. - The values include the returned key or signature.
- They do not include one-time runtime capability detection, which adds up to 4.4 KiB on first use in Linux native builds. Linux values also do not include libc memory routines.
- QEMU stack painting on RV32 and Cortex-M stays within every static bound.
- Other compilers, profiles, and caller inlining can change these values. They do not bound Windows, macOS, or WASM builds.
- Compact signing decodes the secret polynomials on the stack. Prepared signing reads them from storage.
- A core-only build does not prove that signing fits a specific microcontroller.
When a function returns a secret key by value, the bytes move, and a Rust move leaves the old bytes behind. On QEMU RV32 and Cortex-M, a caller that generates a key, signs, and drops the key keeps one unwiped copy of the expanded key in its dead stack frame.
With the alloc feature, these functions take an Allocator and return Box<SecretKey, A>: keypair_from_seed_in, generate_keypair_in, try_generate_keypair_in, SecretKey::try_from_slice_in, SecretKey::from_pkcs8_der_in, and Seed::keypair_in.
- They generate or copy the key directly into that allocation, so moving the box moves only a pointer.
- The box clears the key on drop. A failed call clears it before it returns.
- In the same measurement, these constructors leave no copy of the key on the stack or in the freed allocation.
- Allocation failure behaves as it does for
Box::new_in. - Pass
Global, or an allocator that supplies locked or dump-excluded memory.rscryptodoes not supply such an allocator. - The by-value constructors stay for core-only use.
serde-secretsdeserialization still builds a by-value key.
- Matrix sampling examines at most 298 candidates (894 SHAKE bytes).
- Secret-noise sampling consumes all 481 bytes. It compacts accepted coefficients with fixed addresses.
- Unpublished-challenge sampling consumes all 221 bytes and uses fixed scans.
- Verification samples its public challenge directly.
- Signing tries at most 821 candidates, with at most 5747 mask nonces for ML-DSA-87.
- Every exhausted sampler or signing loop returns
RejectionLimit:MlDsaKeyError::RejectionLimitduring expanded-key import, otherwiseMlDsaError::RejectionLimit. There is no fallback signature and no partial private output.
The threat model does not change. This work makes no new constant-time claim for whole operations.
- Polynomial arithmetic uses fixed coefficient traversals.
- Signing rejects aggregate norms. It packs candidate hints only after every rejection check passes.
- Secret polynomial, SHAKE, byte-buffer, and rejected-output owners use the existing volatile cleanup.
- After each private SHAKE256 call, a fixed 2 KiB volatile scrub clears the dead stack below the hashing helper. ML-KEM shares this helper. Capability detection runs before the helper starts.
- The scrub reaches the Keccak spill slots that the compiler makes only
if the linked frames fit within that bound.
Each target therefore needs frame review (
just stack-frames). - These source properties need compiler and timing evidence for each target.
Target qualification must keep these boundaries:
- Qualify the secret-sensitive arithmetic and the fixed-work samplers in the final artifact.
- Standard rejection sampling returns the first accepted candidate. Whole signing therefore has a variable execution time. It has no strict constant-time claim under the CT policy.
- Agreement with an external library does not prove side-channel resistance. Machine-code review and native timing evidence must name the operations, compiler, and target they cover.
ct.tomlkeeps whole ML-DSA signing best effort, and requires separate evidence for its secret-sensitive kernels. Its timing cases cover forward and inverse transforms, products, accumulation, the fused row product of prepared signing, norm checks, rounding, secret samplers, and valid-key preparation for all parameter sets.- Separate inverse-NTT and Montgomery cases run portable arithmetic.
- Retained C ABI roots support review of the linked code and its call closure.
- The bounded BINSEC root covers only the portable Montgomery leaf. It does not cover full transforms, accelerated kernels, samplers, or signing.
- Registration defines the required evidence. Each target still needs passing results for the exact candidate.
- Reduce the signing stack for constrained targets, and collect device evidence for the measured
bounds.
Prepared storage is written in place, including allocated storage from
prepare_in. Its secret polynomials andKare cleared on prepared-key drop, on failed preparation, and on storage drop. Move, register, and spill copies that the compiler makes stay outside the general cleanup claim. - Test bounded-sampler and signing exhaustion through production paths. Extend the fuzz and corpus coverage, and run the remaining native and device targets. The first native and portable suites, the feature and MSRV matrix, the packaging consumers, and the Wasmtime scalar and SIMD128 vector lanes pass. They do not qualify every target.
- Complete comparable workloads: prepared and unprepared,
representative seeds and signing tails, pure and prehash modes, and
portable-onlymeasurements. Keep the exact source, compiler, CPU, statistics, and profile artifacts. One repeated deterministic signature does not describe the distribution of signing latency.
The portable implementation defines the behavior and stays the fallback. All paths use the same canonical coefficients and generated roots.
| Build | Backend |
|---|---|
macOS and Linux AArch64, compile-time NEON, no portable-only |
Original NEON transforms, products, accumulation, fused row product, matrix rejection sampling, norm checks, rounding, hint computation and application, and SampleInBall scans. |
Linux and Windows x86-64, no portable-only |
Original AVX-512F transforms, products, and fused row product when available, with AVX2 fallbacks. AVX2 also handles accumulation, norm checks, rounding, hint computation, and SampleInBall scans, after cached CPU and OS capability detection. |
Linux IBM Z and little-endian POWER, no portable-only |
Original z/Vector or POWER8 vector transforms, products, and accumulation, after cached CPU and OS capability detection. |
RISC-V32 and RISC-V64, compile-time M, no portable-only |
Original scalar Montgomery assembly within the common scalar transform schedule. No vector extension is needed. |
| All other configurations | Portable arithmetic. |
- IBM Z and POWER share one four-lane transform schedule. The widening products and masked reductions use original register-only assembly. This prevents compiler scalarization and reduction branches that depend on coefficients.
- Register barriers protect scalar reduction and selection on IBM Z and RISC-V,
also with
portable-only. - Without
portable-only, public matrix expansion groups four SHAKE128 streams on capable Linux x86-64. ML-DSA-65 groups three on Linux AArch64 with SHA-3 instructions. Those paths use four or three public polynomial buffers. macOS AArch64 and the remaining supported native configurations retain paired expansion, with scalar permutations when the shared SHAKE backend requires them. Other targets andportable-onlyexpand one stream at a time. The existing SHAKE dispatch and paired/scalar fallbacks remain available. Prepared operations keep the same key-owner sizes. - On every target, signing derives its mask polynomials two SHAKE256 streams at a time. For an odd L, the last polynomial uses one stream.
portable-onlystops runtime capability selection. A build that enables SHA-3 instructions at compile time can still use them in shared SHAKE. Benchmark claims must therefore record the compiler target features as well as the Cargo features.
Architecture qualification and comparative performance work are still open.