Table of Contents

SLH-DSA Test Vectors

Source

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

The vectors are stored in NIST's own ACVP JSON schema in tests/Security/Cryptography/TestData/slhdsa-acvp-fips205.json.gz, and read at run time by SlhDsaAcvpVectors with System.Text.Json — the same arrangement as ML-DSA and ML-KEM. 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. Regenerate it with scripts/fetch-slhdsa-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 SLH-DSA groups are kept — the ones the library implements. The ACVP files also carry pre-hash (HashSLH-DSA) and internal-interface groups; skipping them takes 1,248 cases down to 456.

Why this one is stratified, and how to run everything

For ML-KEM and ML-DSA the interface filter is the whole story: everything runnable is committed. SLH-DSA cannot work that way. Its signatures are 7,856 to 49,856 bytes each, so those 456 cases are 20.6 MB of JSON, gzipping to 11.8 MB — a permanent binary blob several times the size of the entire rest of the test data.

What ships instead is a stratified 180-case selection (~2.15 MB), chosen by rule rather than by cap:

Mode Kept Rationale
keyGen all 120 cases, all 12 sets Keys are seeds, not signatures — the whole document is 0.02 MB gzipped
sigGen 1 case per group = 24 12 sets × {deterministic, hedged}: every set byte-exact in both variants
sigVer 36 cases One valid case for every set, plus all 7 failure reasons on four representative sets

The four sets carrying the full failure matrix are SLH-DSA-SHA2-128f, SLH-DSA-SHAKE-128f, SLH-DSA-SHA2-192s and SLH-DSA-SHAKE-256f — between them they span both hash instantiations, both speed variants, all three security categories, and the SHA-512 split that SlhDsaHash applies for categories 3 and 5.

Nothing is unreachable. Run the script with the full profile and the loader picks the result up automatically:

python scripts/fetch-slhdsa-acvp-vectors.py --profile full
dotnet test tests/Security/Cryptography/Cryptography.Tests.csproj --filter "FullyQualifiedName~SlhDsaAcvp"

SlhDsaAcvpVectors resolves its source in order: the CRYPTOHIVES_SLHDSA_ACVP_VECTORS environment variable, then slhdsa-acvp-fips205.full.json.gz at its default path in the repository (gitignored), then the embedded stratified resource. It writes which one it used to the run log, and VectorFile_CoversAtLeastTheStratifiedSelection fails if an override ever carries fewer cases than the committed file — a truncated download must not quietly shrink the suite. The .github/workflows/acvp-full-vectors.yml job does all of this weekly.


Coverage

External interface, pure SLH-DSA (no pre-hash), for all 12 parameter sets:

Operation Cases (stratified / full) What is verified
Key generation (AFT) 120 / 120 (SK.seed, SK.prf, PK.seed) → byte-exact pk and sk
Deterministic signing (AFT) 12 / 84 (sk, message, context, opt_rand = PK.seed) → byte-exact signature
Hedged signing (AFT) 12 / 84 (sk, message, context, ACVP additionalRandomness) → byte-exact signature
Verification (AFT) 36 / 168 Valid signatures accepted; modified signature — R / SIGFORS / SIGHT, modified message, and signature too large / too small cases rejected

Each operation runs through both paths, as ML-DSA does: the internal SlhDsaCore for byte-exactness, and the key-holding SlhDsa API that mirrors the in-box type. Cases for the s parameter sets are categorized Slow, so --filter "TestCategory!=Slow" gives a fast pass; they run by default.

The keyGen vectors for all 12 sets exercise both hash instantiations end-to-end — including the SHA-2 compressed-ADRS addressing, MGF1-based H_msg, HMAC-based PRF_msg, and the SHA-256/SHA-512 split for categories 3 and 5 — because PK.root is the root of a full XMSS treehash.

Cross-Validation (Interop)

SlhDsaInteropTests cross-validates against independent implementations on every target framework (fast sets):

Peer Tests
BouncyCastle 2.6+ Hedged sign ↔ verify round-trips in both directions; a BouncyCastle-generated secret key imported into our SlhDsa produces signatures BouncyCastle verifies
.NET 10 System.Security.Cryptography.SlhDsa (where OS-supported) Cross sign/verify in both directions including context binding

Sample Vector

Key Generation (SLH-DSA-SHAKE-128f, ACVP keyGen tcId 31)

skSeed = 3956AB391B4D22FC907AF0740326D061
skPrf  = AB0EB206436F2B86EBE086D77739B3E4
pkSeed = 56505C229F4E7FA6B201714C7DCC9DA3
pk     = 56505C229F4E7FA6B201714C7DCC9DA366578F1F24C3FE371C97C14CE0E79CDC
sk     = 3956AB391B4D22FC907AF0740326D061AB0EB206436F2B86EBE086D77739B3E4
         56505C229F4E7FA6B201714C7DCC9DA366578F1F24C3FE371C97C14CE0E79CDC

The key layout is visible directly: pk = PK.seed ‖ PK.root and sk = SK.seed ‖ SK.prf ‖ PK.seed ‖ PK.root, where PK.root (66578F…) is the recomputed hypertree root. Signatures run to 7.8–49.9 KB; see the data file for complete sigGen/sigVer vectors.


Regenerating the Vectors

python scripts/fetch-slhdsa-acvp-vectors.py                 # stratified, overwrites the committed file
python scripts/fetch-slhdsa-acvp-vectors.py --profile full  # complete set, gitignored
python scripts/fetch-slhdsa-acvp-vectors.py --limit 2       # ad-hoc: additionally cap cases per group

The script downloads the three internalProjection.json files listed above (~69 MB in total; the sigGen file alone is 38 MB and carries the additionalRandomness values for the hedged cases), applies the signatureInterface: external / preHash: pure filter, applies the profile's selection rule, and writes deterministic gzip. Because the stored schema is the upstream one, enabling a currently skipped group later is a change to the script's filter rather than to the file format.


HashSLH-DSA (Pre-Hash) Vectors

The pre-hash groups (FIPS 205 §10.2) live in their own file, tests/Security/Cryptography/TestData/slhdsa-prehash-acvp-fips205.json.gz, loaded by SlhDsaPreHashAcvpVectors and exercised by SlhDsaPreHashAcvpTests.

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

The size problem is sharper here than anywhere else in the suite. The runnable pre-hash set is 456 cases and 15.9 MB gzipped, and even the full cross product of 12 parameter sets by 12 approved pre-hash functions would be 144 sigGen cases and roughly 5 MB. The committed file is therefore a 37-case selection (~1.38 MB), chosen by rotation rather than by cap:

Mode Kept Rule
sigGen 24 One case per group, rotating the pre-hash function by parameter-set index. All 12 sets × {deterministic, hedged}, and all 12 pre-hash functions, in 24 cases.
sigVer 13 The valid case on that same rotation for every set, plus all 6 failure reasons on SLH-DSA-SHA2-128f and SLH-DSA-SHAKE-256f.

Two representative sets rather than the four the pure profile uses, and deliberately so: the failure-reason matrix is a property of SLH-DSA verification that the pure profile already covers in full. What pre-hash adds is the OID-bound prefix, and these two span both hash instantiations and the SHA-512 split that SlhDsaHash applies for categories 3 and 5.

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.

Running the complete set

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

Same escape hatch as the pure set: the gitignored *.full.json.gz is preferred automatically, or name it in CRYPTOHIVES_SLHDSA_PREHASH_ACVP_VECTORS. VectorFile_CoversAtLeastTheCommittedSelection fails if an override carries fewer cases than the committed file, and the weekly acvp-full-vectors workflow runs all 456. Expect it to be slow: the s sets sign at roughly 10⁶ hash invocations each, which is why it is weekly rather than per-PR.

Usage

These test vectors are used by the unit tests in tests/Security/Cryptography/Dsa/SlhDsa/ to verify FIPS 205 conformance of the SLH-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.