Skip to content

mldsa_audit

xero edited this page May 30, 2026 · 1 revision
logo

ML-DSA Cryptographic Audit

Audit of the leviathan-crypto WebAssembly ML-DSA implementation (AssemblyScript) against FIPS 204, covering all three parameter sets (ML-DSA-44, ML-DSA-65, ML-DSA-87) and verified against NIST ACVP vectors.

Table of Contents

Meta Description
Target: leviathan-crypto WebAssembly implementation (AssemblyScript)
Spec: FIPS 204 (ML-DSA Standard, August 2024)
Parameter sets: ML-DSA-44, ML-DSA-65, ML-DSA-87
Test vectors: NIST ACVP (ML-DSA-keyGen-FIPS204, ML-DSA-sigGen-FIPS204, ML-DSA-sigVer-FIPS204)

HashML-DSA Prehashed-Input Surface

MlDsaBase exposes four methods that accept a caller-supplied prehash digest in place of the raw message: signHashPrehashed, signHashPrehashedDeterministic, signHashPrehashedDerand, and verifyHashPrehashed. They share the post-PH path with the non-prehashed signHash family via two internal helpers, signWithPrehash (in src/ts/mldsa/sign.ts) and verifyWithPrehash (in src/ts/mldsa/verify.ts).

Audit checklist:

  • signHashPrehashed validates digest size against digestSize(ph) before signing; mismatch throws SigningError('sig-malformed-input').
  • signHashPrehashedDeterministic validates digest size and uses rnd ← 0³² per FIPS 204 §3.4 (a fresh new Uint8Array(32) is already zeroed; no manual fill needed).
  • signHashPrehashedDerand validates both rnd.length === 32 and digest.length === digestSize(ph); passes caller-supplied rnd to signWithPrehash without modification.
  • signHashPrehashed wipes the hedged-rnd Uint8Array in a finally block; deterministic and derand variants do not own rnd (zeros / caller-supplied) and so do not wipe.
  • verifyHashPrehashed returns false (no throw) on:
    • wrong-length vk,
    • wrong-length sig,
    • wrong-size digest,
    • non-Uint8Array digest.
  • verifyHashPrehashed throws RangeError only on caller-side contract violations (ctx.length > 255, unsupported ph), mirroring the §3.6.2 posture of verifyHash.
  • All four methods call _assertNotOwned('sha3') and _assertNotOwned('mldsa'), plus _assertNotOwned('sha2') when algoNeedsSha2(ph) is true (via _assertHashPrereqs).
  • _assertHashPrereqs validates ph (via digestSize(ph)) before any sha2-initialization check, so widened-type callers (e.g. parsing vector files via as PreHashAlgorithm) hit the canonical "unsupported HashML-DSA pre-hash" RangeError rather than a downstream sha2-not-initialized error.
  • signWithPrehash builds M' = 0x01 ‖ |ctx| ‖ ctx ‖ OID(algo) ‖ prehash via constructMPrimeHash and wipes M' in finally; it does NOT compute the prehash (caller supplies it) and does NOT wipe prehash or rnd (caller owns).
  • verifyWithPrehash mirrors signWithPrehash's contract on the verify side; returns whatever mldsaVerifyInternal returns and wipes M' in finally.
  • The refactor preserves byte-identical output for the existing signHash / signHashDeterministic / signHashDerand / verifyHash methods. Coverage: test/unit/mldsa/hashvariant.test.ts (Gates 8/9/10 across 3 parameter sets × 12 pre-hash functions × 90 ACVP sigGen vectors + 90 ACVP sigVer vectors) all pass unchanged.
  • Equivalence with the non-prehashed family is asserted in test/unit/mldsa/mldsa-prehashed.test.ts: signHashDeterministic(sk, M, ph, ctx) and signHashPrehashedDeterministic(sk, Hash(M, ph), ph, ctx) produce byte-identical signatures across all 36 (paramSet × ph) tuples; cross-API verify (sign via prehashed → verify via non-prehashed and vice versa) succeeds.
  • ACVP sigGen vectors with preHash=preHash are re-oracled through signHashPrehashedDeterministic / signHashPrehashedDerand (with externally-computed PH) and produce byte-identical signatures to the canonical vector.

Sign_internal Rejection-Path Coverage

FIPS 204 §6.2 Algorithm 7 ML-DSA.Sign_internal drives a rejection-sampling loop with four reject conditions: ‖z‖∞ ≥ γ₁ − β, ‖r₀‖∞ ≥ γ₂ − β, hint popcount ‖h‖₁ > ω, and (ML-DSA-44 only) ‖ct₀‖∞ ≥ γ₂. Random AFT sampling triggers these branches too rarely to give meaningful correctness assurance; ACVP ML-DSA JSON Specification §6.1.2 supplies KATs that exercise each path deterministically.

Audit checklist:

  • Rejection-path coverage. Every reachable reject branch on every parameter set is exercised by at least one KAT vector in test/vectors/mldsa_siggen_kats.ts. Source: ACVP ML-DSA JSON Specification §6.1.2 Table 1 (5 vectors per parameter set, 15 total). Coverage: test/unit/mldsa/mldsa_siggen_kats.test.ts records labelled t1-seed-*.
  • High-rejection-count coverage. At least two KAT vectors per parameter set force ‖rejection-count‖ ≥ 32 (the ACVP §6.1.2 SHALL threshold) to confirm the signing loop tolerates heavy retry without early abort. Source: ACVP ML-DSA JSON Specification §6.1.2 Table 2. Recorded counts range 64-100 (ML-DSA-44 has 2 vectors at 77 and 100; ML-DSA-65 has 5 vectors at 64-73; ML-DSA-87 has 5 vectors at 64-69). Coverage: test/unit/mldsa/mldsa_siggen_kats.test.ts records labelled t2-rej-*.
  • Per-KAT byte equivalence. For every vector, KeyGen(seed) reproduces (pk, sk) whose SHA2-256(pk ‖ sk) matches the spec; Sign_internal(sk, M′, rnd = 0³²) produces σ whose SHA2-256(σ) matches the spec. The spec stores hashes rather than full bytes to bound vector-file size; the test reconstructs both halves and compares hashes.
  • Vector provenance. ML-DSA-65 and ML-DSA-87 Table 1 entries reflect the Nov 19 2025 ACVP regeneration (usnistgov/ACVP@f66d187, "Fixes ML-DSA tables with test cases that cover what they are intended to cover") that superseded the originally published vectors flagged on PQC-Forum. The ML-DSA-44 set was correct in the original specification and is unchanged.
  • Spec authority. Vectors are transcribed verbatim from usnistgov/ACVP/src/ml-dsa/sections/04-testtypes.adoc (the asciidoc source for the live spec at https://pages.nist.gov/ACVP/draft-celi-acvp-ml-dsa.html); the SHA256SUMS entry pins the vector file against further drift.

Cross-References

Document Description
mldsa ML-DSA public API reference, including the prehashed surface
audits Project audit index
architecture Repository structure, build and CI, WASM modules, public API, test suite, and security posture

Leviathan-Crypto Wiki

Leviathan logo

Getting Started

Authenticated Encryption

Digital Signatures

Ciphers

  • Serpent-256 TypeScript | WASM
    • Serpent, SerpentCtr, SerpentCbc, SerpentGenerator
  • ChaCha20 TypeScript | WASM
    • ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, ChaCha20Generator
  • AES TypeScript | WASM
    • AES, AESCbc, AESCtr, AESGCM, AESGCMSIV, AESGenerator

Signature Primitives

  • ML-DSA TypeScript | WASM
    • pure (FIPS 204): MlDsa44, MlDsa65, MlDsa87
    • pure-mode suites: MlDsa44Suite, MlDsa65Suite, MlDsa87Suite
    • prehash suites: MlDsa44PreHashSuite, MlDsa65PreHashSuite, MlDsa87PreHashSuite
  • SLH-DSA TypeScript | WASM
    • pure (FIPS 205): SlhDsa128f, SlhDsa192f, SlhDsa256f
    • pure-mode suites: SlhDsa128fSuite, SlhDsa192fSuite, SlhDsa256fSuite
    • prehash suites: SlhDsa128fPreHashSuite, SlhDsa192fPreHashSuite, SlhDsa256fPreHashSuite
  • Ed25519 TypeScript | WASM
    • Ed25519 (pure + Ed25519ph), Ed25519Suite, Ed25519PreHashSuite
  • ECDSA-P256 TypeScript | WASM
    • EcdsaP256 (hedged + RFC 6979), EcdsaP256Suite
    • DER codec: ecdsaSignatureToDer, ecdsaSignatureFromDer, encodeEcPrivateKey, decodeEcPrivateKey, pointDecompress
  • Hybrid composites PQ-only | Classical+PQ
    • PQ-only: MlDsa44SlhDsa128fSuite, MlDsa65SlhDsa192fSuite, MlDsa87SlhDsa256fSuite
    • Classical+PQ: MlDsa44Ed25519Suite, MlDsa65Ed25519Suite, MlDsa44EcdsaP256Suite, MlDsa65EcdsaP256Suite

Key Agreement

Post-Quantum

  • ML-KEM TypeScript | WASM
    • MlKem512, MlKem768, MlKem1024
  • Ratchet (SPQR)
    • KDFChain, ratchetInit, kemRatchetEncap, kemRatchetDecap, RatchetKeypair, SkippedKeyStore

Hashing

  • Hashing overview
  • SHA-2 TypeScript | WASM
    • SHA256, SHA384, SHA512, SHA224, SHA512_224, SHA512_256
    • HMAC_SHA256, HMAC_SHA384, HMAC_SHA512, HKDF_SHA256, HKDF_SHA512
  • SHA-3 TypeScript | WASM
    • SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256
  • BLAKE3 TypeScript | WASM
    • BLAKE3, BLAKE3Stream, BLAKE3KeyedHash, BLAKE3KeyedHashStream
    • BLAKE3DeriveKey, BLAKE3DeriveKeyStream, BLAKE3OutputReader, BLAKE3Hash
  • KMAC
    • CSHAKE128, CSHAKE256, KMAC128, KMAC256, KMACXOF128, KMACXOF256

Transparency Log

  • Merkle
    • MerkleVerifier, MerkleLog
    • SignedLog, Sha256Tree, Blake3Tree, MemoryStorage

Utilities

  • Fortuna CSPRNG
    • Fortuna, SerpentGenerator, ChaCha20Generator, AESGenerator, SHA256Hash, SHA3_256Hash, BLAKE3Hash
  • Utils TypeScript | WASM
    • constantTimeEqual, randomBytes, wipe, encoding helpers
  • TypeScript interfaces
    • Hash, KeyedHash, Blockcipher, Streamcipher, AEAD, Generator, HashFn

Project

Reference

Clone this wiki locally