ML-DSA Verification
Verify post-quantum ML-DSA (Dilithium) signatures in zero knowledge at all FIPS 204 levels, many per proof, without revealing the key or the signature.
On this page
The ML-DSA chiplet proves that signatures pass FIPS 204 verification at ML-DSA-44, ML-DSA-65 and ML-DSA-87, with the
public key and the signature kept in the witness. One pipeline verifies many signatures in a single proof. It
shares hekate-pqc with the ML-KEM chiplet.
§ What the proof states
Each call runs ML-DSA.Verify_internal, FIPS 204 Algorithm 8, on the encoded public key pk, the message
representative M′ and the encoded signature σ. The caller forms M′ from the message M and the context string the way
FIPS 204 Algorithm 3 does, and passes it to the call.
A verified call returns the two digests Algorithm 8 computes on the way: tr = H(pk, 64) and μ = H(tr ‖ M′, 64). The host program decides what becomes public. The example program publishes M′ and tr and keeps pk and σ in the witness: the proof says that the key whose hash is tr signed M′, and reveals neither the key nor the signature.
An invalid signature has no witness. Trace generation stops when the recomputed challenge c̃′ differs from the c̃ in the signature, and the constraints check the same equality inside the proof.
- Parameter sets: ML-DSA-44, ML-DSA-65 and ML-DSA-87.
- Signatures per proof: 1 to 256, or 1 to 227 at ML-DSA-87.
- M′ length: fixed per call when the tables are compiled.
§ How the tables split the work
Six tables carry a verification. The host program trades 32-bit words with the ctrl table over one service bus. Behind it, the Sampler, Keccak, Codec, NTT and HighBits tables pass coefficients, words and Keccak lanes to each other over internal buses, where every value is emitted once by its producer and once by its consumer. Each table is an AIR over the binary tower fields, proven in GF(2^128), and the lattice arithmetic mod q runs on bit-decomposed carry chains.
§ Outside the proof
- Key generation and signing. The chiplet verifies only.
- Forming M′. The proof starts at M′. A statement about M itself needs the host program to constrain how M′ was built from M and the context.
- HashML-DSA pre-hashing. A caller can pass the M′ that HashML-DSA forms (FIPS 204 Algorithm 5); the proof then
covers
Verify_internalon it, and the hash that produced PH(M) stays outside.
§ How it is tested
The tests sign with RustCrypto's ml-dsa crate and verify through the pipeline at all three parameter sets, with
empty and 100-byte messages and with two calls in one proof. Forgery tests change one value at a time, such as a hint
bit, a matrix entry or a host word, and check that each change breaks the constraint or bus that guards it. A message
the key never signed breaks the c̃ comparison.
§ Examples
hekate/examples/mldsa.rs verifies one signature at the level HEKATE_LEVEL selects and publishes M′ and
tr. From a hekate checkout:
HEKATE_LEVEL=65 public builds the variable-time table-math prover that the README's measurements use. Without it the recipe builds
the constant-time prover, the one to use when the witness is private.
The program below is the example at ML-DSA-65, in four files:
src/program.rsdeclares the host table that feeds words to the ML-DSA tables, attaches those tables under one namespace and publishes M′ and tr.src/trace.rsfills the host table.src/signer.rsgenerates a key and signs M′ withsign_internal(FIPS 204 Algorithm 7) from the RustCryptoml-dsacrate, standing in for the signer.src/main.rsbuilds M′ from the message and an empty context, traces the verification, proves and verifies.
Start from the crates in Installation and add hekate-pqc, ml-dsa and
rand.
1use errors;
2use ;
3use ;
4use ;
5use ;
6use Error;
7
8pub const PARAMS: MlDsaParams = ML_DSA_65;
9
10const DIGEST_WORDS: usize = 16;
11
12pub type F = Block128;
13
14define_columns!
20
21
33
34
1use crateVerifyColumns;
2use errors;
3use ;
4use ;
5
6
1use ;
2use ;
3use Error;
4
5
9
10
1
2
3
4
5use Config;
6use DefaultHasher;
7use Transcript;
8use ;
9use program_id;
10use ;
11use prove;
12use preflight;
13use HekateVerifier;
14use ;
15use ;
16use sign;
17use Error;
18use host_trace;
19
20const LABEL: & = b"mldsa65";
21const MESSAGE: & = b"Hekate ML-DSA-65 verification example";
22const CONTEXT: & = b"";
23
24
§ Performance
Zero-knowledge proofs, measured on Apple M3 Max, 16 cores. Build: --release, std parallel blake3 table-math public. Best of three on an otherwise idle machine. The Benchmarks page has the base-protocol figures and the command for each run.
| Workload | Input | Proving | Verify | Proof size | Peak memory |
|---|---|---|---|---|---|
| ML-DSA-44 | FIPS 204 | 433 ms | 10.2 ms | 1,537 KiB | 249 MiB |
| ML-DSA-65 | FIPS 204 | 478 ms | 10.1 ms | 1,575 KiB | 236 MiB |
| ML-DSA-87 | FIPS 204 | 624 ms | 11.1 ms | 1,810 KiB | 332 MiB |
§ Security
The crate has not been audited and may contain bugs and security flaws, as its README states. Hekate proves 100 bits of soundness at every parameter set, below the security level of each, which makes the proof the weaker link; the verified signature is still full ML-DSA.
§ License
hekate-pqc is AGPL-3.0-only. Commercial licenses are available from Oumuamua Labs at
info@oumuamua.dev. The README's Usage section builds an ML-DSA-65 host program,
and the API reference is on docs.rs.