Table of Contents

ML-DSA Test Vectors

Source

NIST ACVP (Automated Cryptographic Validation Protocol) validation vector sets for FIPS 204:

The vectors are stored in NIST's own ACVP JSON schema in tests/Security/Cryptography/TestData/mldsa-acvp-fips204.json.gz, and read at run time by MLDsaAcvpVectors with System.Text.Json. The file holds the upstream keyGen, sigGen and sigVer documents with their testGroups and tests arrays exactly as published, behind a small envelope recording provenance. Every NUnit case is named after its parameter set and the tcId of the original ACVP vector file, so a failure traces straight back to the NIST source.

It is a data file rather than C# literals because ACVP messages run to several kilobytes each and an ML-DSA-87 signature is 4.6 KB per case — inline they added up to 448 KB of test source that dwarfed the tests themselves. It is gzipped because the JSON is ~4.4 MB, compressing to ~2.5 MB. Regenerate it with scripts/fetch-mldsa-acvp-vectors.py, which zeroes the gzip mtime so regenerating unchanged vectors produces a byte-identical file rather than a spurious diff.

Only the external-interface, pure ML-DSA groups are kept — the ones the library implements. The ACVP files also carry pre-hash (HashML-DSA), internal-interface and external-μ groups; skipping them takes sigGen from 360 cases to 90 and sigVer from 180 to 45. Every case that can actually be run is included, so there is no curated subset to revisit — and because the stored schema is the upstream one, enabling a skipped group later is a one-line change to the script's filter rather than a change to the file format.


Coverage

For each of ML-DSA-44, ML-DSA-65, and ML-DSA-87 (external interface, pure ML-DSA — no pre-hash, no external μ):

Operation ACVP group Cases What is verified
Key generation (AFT) keyGen 25 Seed ξ → byte-exact pk and sk
Deterministic signing (AFT) sigGen, deterministic = true 15 (sk, message, context) → byte-exact signature
Hedged signing (AFT) sigGen, deterministic = false 15 (sk, message, context, ACVP-provided rnd) → byte-exact signature, exercised through the internal interface
Verification (AFT) sigVer 15 Valid signatures accepted; modified signature — commitment, modified signature — z, modified signature — hint, and modified message cases rejected

The sigVer modified hint cases exercise the strict HintBitPack validation required for strong unforgeability.

Both API levels

Each operation is exercised twice where the API allows it: once through the stateless IDsa interface and once through the key-holding MLDsa API that mirrors System.Security.Cryptography.MLDsa. 420 test cases in total.

One asymmetry is deliberate. Reproducing a sigGen vector byte for byte needs either deterministic signing (rnd = 0³²) or an injected rnd, and MLDsa signs hedged-only — matching the in-box type, which exposes no deterministic mode either. Byte-exact sigGen therefore lives on the IDsa path; what the MLDsa pass adds is that the key-holding API agrees with NIST about which signatures are valid, over a key imported from an expanded private key.

Cross-Validation (Interop)

MLDsaInteropTests cross-validates against independent implementations on every target framework:

Peer Tests
BouncyCastle 2.6+ Same-seed key generation produces byte-identical pk and expanded sk (MLDsaPrivateKeyParameters.FromSeed); deterministic signatures match byte-for-byte (MLDsaSigner(…, deterministic: true)); hedged sign ↔ verify round-trips in both directions
.NET 10 System.Security.Cryptography.MLDsa (where OS-supported) Same-seed keys match the CNG/OpenSSL implementation byte-for-byte (ImportMLDsaPrivateSeed); cross sign/verify in both directions including context binding

Sample Vectors

Complete vectors are thousands of hex characters; the samples below show short values in full and truncate keys/signatures (lengths noted). Full data: tests/Security/Cryptography/TestData/mldsa-acvp-fips204.json.gz or the ACVP repository.

Key Generation (ML-DSA-44, ACVP keyGen tcId 1)

seed = D71361C000F9A7BC99DFB425BCB6BB27C32C36AB444FF3708B2D93B4E66D5B5B
pk   = B845FA2881407A59183071629B08223128116014FB58FF6BB4C8C9FE19CF5B0B... (1312 bytes)
sk   = B845FA2881407A59183071629B08223128116014FB58FF6BB4C8C9FE19CF5B0B... (2560 bytes)

(pk and sk share the same 32-byte prefix because both encodings begin with ρ.)

Deterministic Signing (ML-DSA-65, ACVP sigGen tcId 36)

message   = 8F (1 byte)
context   = E9E7AA56C7975DC9B4DDBA302B0045BB7A6DEA96E798C6911143 (26 bytes)
signature = FA681CF3153374D98C9F82ABC8691E5E8CE65CF2E8189732A5DE99B8B9159FCE... (3309 bytes)

The context string participates via the FIPS 204 message prefix 0x00 ‖ |ctx| ‖ ctx ‖ M, so these vectors also validate the external-interface domain separation.


Regenerating / Extending the Vectors

python scripts/fetch-mldsa-acvp-vectors.py

The script downloads internalProjection.json from the ACVP folders listed above (it contains both prompts and expected results, including the rnd values for hedged sigGen cases), keeps the groups matching signatureInterface: external, preHash: pure, externalMu: false, and writes the gzipped result. Running it against unchanged upstream vectors produces a byte-identical file, so a real diff means NIST published new vectors.

Pass --limit N to cap the cases per kept group — useful when iterating locally, but the committed file is the complete runnable set. To take on external-μ later, widen is_pure_external() in the script and teach MLDsaAcvpVectors the extra fields: because the stored schema is NIST's own, neither is a file-format change.


HashML-DSA (Pre-Hash) Vectors

The pre-hash groups (FIPS 204 §5.4) live in their own file, tests/Security/Cryptography/TestData/mldsa-prehash-acvp-fips204.json.gz, loaded by MLDsaPreHashAcvpVectors and exercised by MLDsaPreHashAcvpTests. They are kept separate from the pure set rather than merged into it: the selection rule differs, and merging would rewrite an already-committed 2.5 MB blob every time either set is regenerated.

python scripts/fetch-mldsa-acvp-vectors.py --profile prehash

The runnable pre-hash set is 135 cases and 2.11 MB gzipped — affordable, but the committed file is a 57-case stratified selection (~0.88 MB) chosen by rule rather than by cap:

Mode Kept Rule
sigGen 36 Every (parameter set × pre-hash function) pair exactly once — 3 × 12. Even-indexed pre-hash functions are taken from the deterministic group and odd-indexed from the hedged one, so both signing modes are exercised for every set.
sigVer 21 One valid case per (set × pre-hash function), plus every failure reason: modified message, commitment, hint and z.

All twelve approved pre-hash functions and all three parameter sets appear. The digest PH(M) is recomputed from the vector's message by the tests rather than read from the file, so they also cross-check the OID-to-hash binding end to end — a wrong OID table entry or a wrong SHAKE output length fails there rather than silently signing the wrong prefix.

Running the complete set

python scripts/fetch-mldsa-acvp-vectors.py --profile prehash-full
dotnet test tests/Security/Cryptography/Cryptography.Tests.csproj   --framework net10.0 --filter "FullyQualifiedName~MLDsaPreHashAcvpTests"

The script writes a gitignored mldsa-prehash-acvp-fips204.full.json.gz, which the loader prefers automatically — it resolves CRYPTOHIVES_MLDSA_PREHASH_ACVP_VECTORS first, then that default path, then the embedded resource, and logs which it used. VectorFile_CoversAtLeastTheCommittedSelection fails if an override ever carries fewer cases, so a truncated download cannot quietly shrink the suite. The weekly acvp-full-vectors workflow runs all 135 and checks the committed file still regenerates byte-identically.

Usage

These test vectors are used by the unit tests in tests/Security/Cryptography/Dsa/ to verify FIPS 204 conformance of the ML-DSA implementation against known good values from official sources.

License

The test vectors are derived from the public NIST ACVP-Server repository. See the ACVP-Server license (NIST software is public domain) for terms.