← Back to Blog

FIPS 204 Walkthrough: ML-DSA Standard in Plain English

FIPS 204 Walkthrough: ML-DSA Standard in Plain English - QNSQY post-quantum encryption guide

FIPS 204 is the Module-Lattice-Based Digital Signature Standard. It was published by NIST on August 13, 2024, the same day as FIPS 203. While FIPS 203 covers key encapsulation, FIPS 204 covers signatures. Where its sibling decides who is allowed to read a message, FIPS 204 decides who is allowed to claim authorship.

Think of FIPS 204 as the standard for a notary's stamp. The stamp must be unique to the notary, hard to forge, and verifiable by anyone holding the public registry of stamps. ML-DSA does that with mathematics that resist quantum attacks.

This walkthrough goes section by section through the public, official text, translating dense math into engineering concepts. The document is 65 pages. Most teams will never read past the table of contents. Yet every part of FIPS 204 affects how signatures interoperate across vendors, hardware modules, and operating systems.

Section 1: Introduction and Why ML-DSA Exists

Section 1 explains the purpose. ML-DSA is a digital signature scheme based on the Module Learning With Errors and Module Short Integer Solution problems. It is the successor to CRYSTALS-Dilithium, the round-three NIST competition winner.

The introduction names the design goals:

  • Strongly unforgeable under chosen-message attack (SUF-CMA)
  • Resistant to both classical and quantum attackers
  • Stateless, so signers do not need to track counters or state files

The last point matters. Stateless signatures avoid the "lost counter" disasters that have hit stateful schemes. Even stateless schemes still require a high-quality randomness source for security, but they do not require persistent state. For comparison, see our coverage of LMS stateful signatures.

Section 1 also mentions that ML-DSA is the recommended general-purpose signature, while SLH-DSA (FIPS 205) and FN-DSA (FIPS 206 draft) cover other use cases. ML-DSA is fast and produces medium-size signatures. It is the workhorse.

Section 2: Notation and Mathematical Conventions

Section 2 mirrors its FIPS 203 counterpart. It defines the polynomial ring R_q where q is 8380417, the prime used throughout ML-DSA.

The conventions matter for interoperability:

  • Polynomials have 256 coefficients
  • Coefficients fit in 23 bits (mod q)
  • Module rank varies per parameter set: 4, 6, or 8

Think of this as setting the bit depth on a recording studio. Once everyone agrees on the bit depth, signatures recorded by one studio play correctly in another.

The section also defines Power2Round, Decompose, HighBits, and LowBits. These functions split coefficient values into "high" and "low" pieces and are used in the rejection sampling step that gives ML-DSA its security proof.

Section 3: Cryptographic Functions Used Inside ML-DSA

Like FIPS 203, FIPS 204 borrows hash functions from FIPS 202. The list is:

  • SHAKE128, used for matrix expansion from a public seed
  • SHAKE256, used for message hashing, randomness expansion, and challenge generation
  • SHA3-256 used for verifying public-key hashes

Notice the heavy reliance on SHAKE256. ML-DSA uses SHAKE256 in extendable-output mode to generate vectors of polynomials, challenge polynomials, and even the final hash that gets signed. The "hash internal" function used during signing is SHAKE256 with domain separation prefixes.

The domain separation is critical. Without it, an attacker could feed the output of one stage as the input of another and break the security proof. Section 3 lists the exact prefix bytes used.

Section 4: Auxiliary Algorithms

Section 4 is the densest part of the document. It covers everything from byte encoding to polynomial sampling. The major pieces are:

  • IntegerToBits and BitsToInteger (Section 8.1.1)
  • BytesToBits and BitsToBytes (Section 8.1.2)
  • CoeffFromThreeBytes, the rejection sampler that generates uniform polynomials in R_q (Section 8.1.3)
  • CoeffFromHalfByte, used for sampling small polynomials (Section 8.1.3)
  • SimpleBitPack and BitPack, the byte-packing routines used for signature encoding
  • HintBitPack and HintBitUnpack, special routines for the "hint" polynomial in signatures
  • SampleInBall, the rejection sampler that produces a sparse challenge polynomial with exactly tau nonzero coefficients

SampleInBall is one of the most important auxiliary algorithms. It controls the security level of the scheme. The challenge polynomial has to look uniformly random while having a fixed Hamming weight. ML-DSA achieves this through Fisher-Yates shuffling driven by SHAKE256 output.

Section 5: ML-DSA Internal Functions

Section 5 defines the algorithms most users actually care about, but with a twist. Each public function is implemented as a wrapper around an "internal" version that takes an explicit randomness input. This separation is what makes the scheme fully testable. The internal functions are deterministic given their inputs.

The three internal functions are:

  • ML-DSA.KeyGen_internal (5.1)
  • ML-DSA.Sign_internal (5.2)
  • ML-DSA.Verify_internal (5.3)

Each is described step by step. Key generation produces a public key pk and a secret key sk. The public key contains a 32-byte seed rho plus a vector of polynomials representing the second component t1. The secret key contains private polynomials, the public seed, and a hash of the public key.

Internal signing takes the secret key, message, and randomness, then runs a rejection-sampling loop. The signer keeps drawing a candidate signature until one passes a series of bound checks. On average, fewer than 5 attempts are needed for ML-DSA-65, but the loop has no fixed upper bound.

Internal verification re-derives the challenge polynomial from the public key, message, and signature, then checks that the signature satisfies the bound conditions. If yes, the signature is valid.

Section 6: ML-DSA External Functions

Section 6 wraps the internal functions with the public API. There are two families: pure ML-DSA (Section 6.2) and HashML-DSA (Section 6.3).

Pure ML-DSA hashes the message inside the signature itself. The application passes the original message bytes; the algorithm handles hashing internally. This avoids the "hash-then-sign" trap where collisions in the external hash break the signature.

HashML-DSA is for cases where the application has already hashed the message and only the digest is available. It is sometimes called "prehash" mode. The external hash must be one of a NIST-approved list, and the prefix encodes the OID of that hash so different prehash functions cannot collide.

The functions are:

  • ML-DSA.KeyGen (6.1)
  • ML-DSA.Sign (6.2)
  • ML-DSA.Verify (6.2)
  • HashML-DSA.Sign (6.3)
  • HashML-DSA.Verify (6.3)

For most applications, pure ML-DSA is the right choice. HashML-DSA is for legacy protocols that pass digests over the network, like some PKCS#11 hardware modules.

Section 7: Algorithm Identifiers and Parameter Sets

Section 7 names the three parameter sets:

  • ML-DSA-44 (NIST security category 2)
  • ML-DSA-65 (NIST security category 3)
  • ML-DSA-87 (NIST security category 5)

The numbers refer to the dimensions of two matrices used internally: 4×4 for ML-DSA-44, 6×5 for ML-DSA-65, and 8×7 for ML-DSA-87.

Public key sizes are 1312, 1952, and 2592 bytes. Signature sizes are 2420, 3293, and 4595 bytes. Compared to Ed25519's 64-byte signatures, this is a significant size penalty, but it is the cost of quantum resistance.

ML-DSA-65 is the recommended general-purpose default. It matches the security level of ML-KEM-768, making it natural to pair them in hybrid deployments.

Section 8 and Annex A: Test Vectors and Conformance

Annex A lists the parameter values for each set: q, n, d, tau, gamma_1, gamma_2, k, l, eta, beta, omega. These constants must match exactly across implementations. A single off-by-one in tau causes signatures from one implementation to fail verification under another.

Section 8 also references the Cryptographic Algorithm Validation Program. Test vectors are published as ACVP JSON files. QNSQY runs against the official ACVP suite to verify byte-for-byte correctness.

How QNSQY Implements FIPS 204

QNSQY uses ML-DSA-65 as its default signing algorithm and offers ML-DSA-87 for high-assurance contexts. We pair ML-DSA with Ed25519 in hybrid mode using a NIST SP 800-227 compliant combiner. Either algorithm can fail without breaking the overall signature.

The Free tier supports ML-DSA-44 only. Pro and Business tiers ship with all three parameter sets, plus the option of SLH-DSA for users who need a hash-based fallback.

For a comparison with stateless and stateful signature alternatives, see ML-DSA vs SLH-DSA, FN-DSA Falcon explained, and our LMS stateful signatures guide.

Domain Separation in Detail

FIPS 204 is unusually strict about domain-separation prefixes. The document specifies that:

  • Public-key hashes use a different prefix than message hashes
  • Pure ML-DSA prefixes message bytes differently from HashML-DSA
  • HashML-DSA includes the OID of the external hash function in the prefix

These rules sound pedantic, but they prevent a class of attacks where an adversary tricks one party into signing what looks like a public key but is actually a chosen message. Without domain separation, the attacker could fabricate forged signatures by replaying values from one context into another.

Implementations migrating from pre-standard Dilithium to FIPS 204 ML-DSA must update the domain-separation prefixes. ACVP tests catch any mismatch immediately.

The Rejection-Sampling Loop in Practice

Section 5 specifies that signing repeatedly draws candidate signatures until one passes a series of bound checks. Each rejection is wasted work, but it is a feature, not a bug. The rejection sampling is what makes the signature a uniformly random element of a small ball, which is what the security proof needs.

In practice, the expected number of iterations is small (typically 4 to 6 for ML-DSA-65), but the worst case has no fixed upper bound. Engineers building real-time systems should account for occasional latency spikes. ML-DSA-87 has the highest expected iteration count due to tighter bounds.

A naive implementation that branches on rejection introduces a timing side-channel: an attacker measuring signing time could infer information about the secret. FIPS 204 conforming implementations randomize the rejection counter and use constant-time selection where possible. The rejection itself reveals only public information, so a bounded leak through timing is acceptable.

Comparing Parameter Set Trade-Offs

The three ML-DSA parameter sets give engineers a meaningful choice:

  • ML-DSA-44: smallest signatures (2420 bytes), fastest signing and verification, 128-bit symmetric strength. Suitable for high-volume use cases like TLS handshakes or per-message signing.
  • ML-DSA-65: medium signatures (3293 bytes), balanced performance, 192-bit symmetric strength. The general-purpose default.
  • ML-DSA-87: largest signatures (4595 bytes), slightly slower signing, 256-bit symmetric strength. For top-secret data and CNSA 2.0 high-assurance contexts.

Verification time grows roughly linearly with signature size, but the absolute differences are small (single-digit milliseconds). Signing time has more variance because of the rejection-sampling loop, with worst-case latency higher for tighter-bound parameter sets.

For interoperability across organizations, ML-DSA-65 has emerged as the de facto standard in IETF drafts and certificate-authority pilots. Most production deployments default to ML-DSA-65 unless a specific compliance requirement forces ML-DSA-87.

FAQ

Why use SHAKE256 inside ML-DSA?

SHAKE256 is an extendable-output function. ML-DSA needs streams of output of varying lengths for matrix expansion and randomness derivation. SHAKE256 provides exactly that with strong domain separation.

What is the rejection sampling loop and why does it matter?

The signer rejects candidate signatures whose internal values fall outside safe bounds. Each rejection adds latency. The expected number of iterations is small, but worst-case latency can spike, which matters for real-time systems.

Is ML-DSA the same as Dilithium?

ML-DSA-65 is similar to Dilithium3, but NIST tightened parameters and added domain separation prefixes. They are not byte-for-byte compatible. Implementations of pre-standard Dilithium will fail interoperability tests against FIPS 204.

Should I use pure ML-DSA or HashML-DSA?

Pure ML-DSA whenever the application has access to the message bytes. HashML-DSA only when forced by a legacy protocol that delivers prehash digests.

When does the FIPS 204 transition deadline arrive?

NSA's CNSA 2.0 sets 2035 as the full mandatory date for federal systems. See our 2035 NSA CNSA deadline plan for the staged timeline.

Sources

  1. FIPS 204, Module-Lattice-Based Digital Signature Standard, NIST, August 13, 2024
  2. FIPS 202, SHA-3 Standard, NIST, August 5, 2015
  3. NIST IR 8413, Status Report on the Third Round of the NIST Post-Quantum Cryptography Standardization Process, NIST, July 2022
  4. CRYSTALS-Dilithium Algorithm Specifications and Supporting Documentation, Round 3 submission, CRYSTALS team, 2020
  5. Cryptographic Algorithm Validation Program (CAVP) - ACVP Test Vectors for ML-DSA, NIST CSRC

Related Articles

Protect Your Data Before Q-Day Arrives

QNSQY's NIST-standardized post-quantum encryption protects files against both current and quantum-era threats.

Try QNSQY