Signature schemes
ML-DSA-87 explained mathematically
How ML-DSA-87 (FIPS 204) signs a McGesund review — from module-LWE through rejection sampling to verification in the browser.
Last updated: 2026-09-07
1. What this is about
When someone submits a review on McGesund, more happens in the background than the text suggests. The review is signed digitally on submission. Every visitor can recompute that signature later in their own browser — without trusting us and without asking us.
For customers from the Pro plan upwards this is done with ML-DSA-87, among others. This article explains what happens mathematically.
One thing up front:
ML-DSA is not encryption. The review text stays publicly readable — that is the point of a review. ML-DSA does not prove secrecy, it proves origin and integrity.
ML-DSA was developed from CRYSTALS-Dilithium and is standardised as FIPS 204. The number 87 denotes the parameter level. There are three:
- ML-DSA-44
- ML-DSA-65
- ML-DSA-87
ML-DSA-87 is the highest and sits in NIST security category 5.
2. What exactly gets signed?
It is not the review text itself that goes into the signature but a compact data object that pins the text down unambiguously. At McGesund it looks essentially like this:
{
"v": 1,
"typ": "rev-comment",
"f": "<company ID>",
"c": "<review ID>",
"h": "<SHA-256 of the review text>",
"rh": "<SHA-256 of the entire submission record>",
"rv": 1,
"qh": "<SHA-256 of the QR envelope, QR reviews only>",
"iat": 1757203200
}
This object is our message . It ties six statements together:
- Which company the review belongs to (
f) - Which review is meant (
c) - What text was behind it — as a hash value (
h) - What record as a whole was submitted (
rh): text, hearts, geo status and occasion details, canonically serialised and hashed — in schema versionrv - Which QR code the review came from (
qh); without a QR the field is omitted - When it was signed (
iat)
If someone later changes a single character of the review text, h no longer fits — and neither does rh. Anyone who instead only moves the hearts leaves h untouched but breaks rh. And if someone changes one of those hash values, the signature no longer fits. That chain is precisely the point.
3. What the signature has to achieve
A visitor reading the review should be able to check three things for themselves:
- The review really was issued by McGesund.
- The text has not been changed since submission.
- Nobody can invent a new, valid-looking review.
That is what the key pair is for:
- a private key — held exclusively by the signing service
- a public key — anyone may have it; it is addressed via the key ID (
kid) in the envelope
Signing uses the private key. Verifying uses the public one — in the reader's browser, not on our server.
4. Why a post-quantum scheme at all?
Many signature schemes in common use today rest on factoring large numbers or on discrete logarithms. A sufficiently powerful quantum computer could solve exactly those problems considerably faster with known algorithms.
For a review this is not an academic question. A review should still be verifiable in ten years. Whoever signs today signs for the entire lifetime of the entry.
ML-DSA therefore uses a different foundation:
More precisely: module-LWE and module-SIS.
5. What is a lattice?
At first, just points in space. Take two vectors:
All integer combinations
form the lattice. For example:
The decisive part:
In small dimensions lattice problems are easy. In very high dimensions certain tasks become extremely difficult.
6. Polynomials instead of single numbers
ML-DSA does not compute with 2D vectors but with polynomials and vectors of polynomials.
A polynomial such as
can be written as a list of coefficients:
The arithmetic happens in a ring:
That means two things:
- : arithmetic modulo
- : an extra rule that fixes the polynomial length
For all three ML-DSA levels:
So a polynomial has 256 coefficients, considered modulo 8,380,417. What changes between the levels is not or but the size of the matrices — more on that later.
As an aside: polynomial multiplication in this ring runs in practice via the NTT, the number-theoretic variant of the fast Fourier transform. ML-DSA therefore by no means does without FFT ideas; they simply sit in the arithmetic rather than in the signature principle.
7. The central trick: module-LWE
The core idea is module learning with errors:
Here:
- — public, seemingly random matrix of polynomials
- — small secret vectors
- — public value
An attacker knows and , but not . To them the equation looks like a random equation with noise. They should not be able to compute the small secrets back from it efficiently.
8. A tiny numeric example
We deliberately take a ridiculously small variant — ordinary numbers instead of polynomials, dimension 2 instead of 256, and
Let
Then:
At this miniature size you could try out every possibility. With ML-DSA-87, is an matrix of polynomials with 256 coefficients each — that is over 14,000 unknowns in the lattice structure.
9. The signing service's key pair
The private key contains, among other things, the small vectors . With ML-DSA-87 their coefficients come from the interval
that is, from . This smallness is not a detail but the core: only because the secrets are small does a hard lattice problem arise at all.
The public key is, simplified,
is a seed from which can be reconstructed deterministically — so the matrix does not have to be transmitted. is the upper bits of ; the lower bits are dropped, which shrinks the key considerably. That omission is the reason for the so-called hints later on.
This produces the asymmetry we want:
10. The review becomes a number
The signing service first hashes the payload object from section 2:
In our toy example we use an artificial mini hash. In the real system is 512 bits long and additionally binds in the public key — which means a signature cannot be reinterpreted as belonging to another key.
11. Commitment
The signing service draws a random small vector . In our example:
From it comes an intermediate value, the commitment:
This is not yet a signature.
12. The review turns into a challenge
Message and commitment are hashed together:
With ML-DSA-87, is a polynomial with exactly coefficients from ; the remaining 196 are zero. This structure is intentional: it keeps small.
In our toy example we simply set
13. The actual signature
With our values:
14. The step that is easily missed: rejection sampling
Here lies the point where ML-DSA differs from a naive construction — and it is not optional.
contains the secret . If you simply always released , then could be extracted statistically from enough signatures. For a review platform with a great many signatures per day that is not a theoretical risk.
The signing service therefore checks before releasing whether gives away too much, and discards the signature otherwise — then it starts over with a fresh random . This is called Fiat-Shamir with aborts.
The condition is essentially:
For ML-DSA-87, and . On top of that comes a second bound on the lower bits. In practice several passes are normal — signing is therefore a loop, not a single step.
Important for verification: the browser checks exactly this bound later on. A signature with coefficients that are too large is rejected even if the equation works out.
15. Why the browser can check this
The reader's browser knows:
- the review and therefore
- the public key
- the signature
It does not know . The relationship that gets it further anyway:
And because
holds, the unknown can be replaced by the public value:
That is the central equation — and it says something important: the browser reconstructs not exactly, but only up to the small term .
16. The miniature example to the end
We had:
Let us work it through:
The original commitment was
The difference is
Exactly the small error term predicted. The verifier does not get but something that lies close to .
That is precisely why ML-DSA does not compare the values themselves but their upper bits. And that is precisely why the signature additionally contains a hint vector : it states compactly at which positions the rounding tipped over a boundary because of the small error term. With ML-DSA-87 at most such hints are allowed. They do not give away the secret — they only repair the rounding.
Finally the browser recomputes the challenge. If it matches,
and all norms lie within the bounds, the signature is valid.
17. What happens if someone changes the review?
Suppose someone with database access — including someone at our company — changes the review text or one of the hearts. Then at least one of the two hash values in the payload changes (h for the text, rh for any field of the record):
That changes the challenge:
But the existing signature was produced for the old challenge. The browser recomputes and finds:
The decisive sentence: we can delete a review, but we cannot alter it unnoticed. At McGesund the same check additionally runs nightly on the server across the whole stock — a review that fails it no longer counts towards the company's average.
18. Why can nobody invent a signature?
An attacker knows and , but not . To build a valid signature they would have to find a triple that
- satisfies the verification equation and
- keeps within the norm bounds and
- matches the challenge that arises from exactly those values themselves.
At its core this amounts to a hard lattice problem — specifically module-SIS: finding short solutions of a homogeneous equation modulo . The smallness condition is not decoration here but the actual difficulty. Without it a solution would be trivial.
19. Why "module"?
The word describes the structure between simple vectors and general lattices. Instead of computing with single numbers, ML-DSA works with vectors of polynomials:
and with matrices of those:
The advantage: you get the high dimension of a lattice while keeping a compact, efficiently computable representation. Security can be tuned via the matrix size without changing the ring.
20. Why 87 of all numbers?
The three levels differ not in the ring but in the dimensions:
| Parameter | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| Matrix size | |||
| Secret range | 2 | 4 | 2 |
| Challenge weight | 39 | 49 | 60 |
| Public key | 1,312 B | 1,952 B | 2,592 B |
| Signature | 2,420 B | 3,309 B | 4,627 B |
| NIST category | 2 | 3 | 5 |
Worth noting: ML-DSA-87 is not simply "ML-DSA-65, only bigger". The secret range goes back down from 4 to 2; the security here comes from the larger matrix, not from larger coefficients. It is an independent, standardised choice of parameters.
The price: 4,627 bytes per signature — per stamp that is stored and delivered to the browser when verifying. That is why FALCON, which manages with 1,280 bytes, is available at McGesund alongside ML-DSA-87.
21. Fiat-Shamir: why it works without a counterpart
An interactive proof would run like this:
- The signing service sends a commitment.
- The verifier sends a random challenge.
- The signing service responds.
- The verifier checks.
With a review there is no such dialogue — the reader arrives months later. The solution is the Fiat-Shamir transformation: the challenge is not rolled but hashed from the data itself:
That turns a dialogue into a document. The signing service cannot pick its challenge, because doing so would mean controlling the hash.
22. The whole process
23. What McGesund does with it in practice
Three layers interlock:
The envelope. Every signed review carries an Ed25519 signature. That is the mandatory variant — classical, tiny, natively verifiable in every browser.
The post-quantum stamps. In addition, one or two quantum-resistant signatures can be placed alongside. Which ones depends on the plan:
| Plan | available signature levels |
|---|---|
| Basis | Ed25519, FN-DSA-512 |
| Klassik | Ed25519, FN-DSA-512, FN-DSA-1024 |
| Pro | Ed25519, FN-DSA-1024, ML-DSA-87 |
| Premium | Ed25519, FN-DSA-1024, ML-DSA-87, both in parallel |
The parallel variant is deliberately redundant. Should one of the two mathematical families — NTRU lattices for FALCON, module lattices for ML-DSA — turn out weaker than assumed today, the other carries on.
The time anchor. The fingerprint of the signing key is anchored in a Bitcoin block via OpenTimestamps. That makes it possible to show not only that the signature is genuine but also that it already existed at a particular point in time — without anyone having to believe our timestamp.
All of this is verified in the reader's browser, via a WASM module. We supply the data; the arithmetic is done on the visitor's device. If we went offline tomorrow, a review once downloaded would remain verifiable.
24. ML-DSA and FALCON side by side
| Property | FALCON (FN-DSA) | ML-DSA |
|---|---|---|
| Type | digital signature | digital signature |
| Lattice family | NTRU | module-LWE / module-SIS |
| Ring | , | , |
| Core mechanism | short vector via Gaussian sampling | challenge-response with aborts |
| FFT / NTT | floating-point FFT, security-critical | NTT, arithmetic only |
| Signature size (highest level) | 1,280 B | 4,627 B |
| Implementation | demanding (floating point) | comparatively straightforward |
| Standardisation | intended as FIPS 206 (FN-DSA), not yet finished | FIPS 204, finished |
In short: ML-DSA is easier to implement and verify correctly, FALCON delivers considerably more compact signatures. Neither of them sits in the QR code — there is only the Ed25519 envelope. Signature size therefore counts for storage and delivery, robustness for implementation. That is why we offer both.
25. One sentence to take away
Whoever holds the secret vector signs in milliseconds. Whoever does not would have to solve a lattice problem in over 14,000 dimensions — even with a quantum computer.
For the reader of a review this simply means: they do not have to believe us. They can do the arithmetic.