Skip to content

asm_mldsa

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

mldsa ML-DSA WASM Module Reference

This low-level reference details the ML-DSA AssemblyScript source and WASM exports, intended for those auditing, contributing to, or building against the raw module. Most consumers should instead use the TypeScript wrapper.

Table of Contents


Overview

This module implements the polynomial-arithmetic, encoding, sampling, and rounding primitives needed for ML-DSA (FIPS 204), as a standalone WebAssembly binary compiled from AssemblyScript. ML-DSA is the NIST post-quantum signature standard published as FIPS 204; the underlying scheme is Dilithium.

The implementation is (k, ℓ)-parameterized: a single mldsa.wasm serves all three FIPS 204 parameter sets (44/65/87). The TypeScript wrapper selects the parameter set per call by passing k and (and other runtime parameters) as arguments to the polyvec/encoding kernels.

Parameter Set k η NIST Security
ML-DSA-44 4 4 2 Category 2
ML-DSA-65 6 5 4 Category 3
ML-DSA-87 8 7 2 Category 5

Key properties of this implementation:

Static memory only. All buffers are fixed offsets in linear memory. The AS compiler places the zetas StaticArray<i32> (256 × 4 B = 1024 B) in the data segment at low memory; mutable regions start at offset 4096. No memory.grow(), no heap allocation. Total memory: 4 pages (256 KiB).

SIMD by default for NTT and poly arithmetic. ntt, invntt, poly_add, poly_sub, poly_reduce, poly_caddq, and poly_pointwise_montgomery resolve to SIMD implementations (4-lane i32×4 butterflies / lane-parallel arithmetic). Scalar variants are exported as *_scalar for cross-checks. Some kernels (poly_freeze, poly_chknorm, the encoding/sampling/rounding paths) remain scalar, see CT-posture notes per file.

i32 coefficients. ML-DSA's prime q = 2²³ − 2¹³ + 1 = 8380417 does not fit in i16, so polynomials are stored as 256 × i32 = 1024 bytes each. Compare to mlkem, where i16 suffices.

No protocol-level logic in WASM. Algorithm 6 (KeyGen), Algorithm 7 (Sign), Algorithm 8 (Verify) are all TS-orchestrated. The WASM module exports the kernels these algorithms compose: matrix expansion, rejection sampling, polynomial multiplication, rounding, encoding.


Buffer Layout

Defined in src/asm/mldsa/buffers.ts. All offsets in bytes from base 0.

Offset    Size      Region
─────────────────────────────────────────────────────────────────────
0..4095   4096      AS data segment (zetas table, 256 × i32 = 1024 B)
4096      8192      POLY_SLOTS    (8 × 1024, POLY_SLOT_0..7 scratch polys)
12288     65536     MATRIX_SLOT   (matrix Â, k×ℓ max = 8×8 polys × 1024)
77824     65536     POLYVEC_SLOTS (8 × 8192, POLYVEC_SLOT_0..7, k=8 max)
143360    128       SEED_OFFSET   (ρ ‖ ρ′ ‖ K, H(ξ‖k‖ℓ, 128) lands here)
143488    64        TR_OFFSET     (tr = H(pk, 64))
143552    64        MSG_REP_OFFSET (μ, message representative)
143616    64        C_TILDE_OFFSET (signature commitment hash, ≤ λ/4)
143680    64        (alignment / reserved)
143744    2624      PK_OFFSET     (≥ 2592 for ML-DSA-87)
146368    4928      SK_OFFSET     (≥ 4896 for ML-DSA-87)
151296    4736      SIG_OFFSET    (≥ 4627 for ML-DSA-87)
156032    8192      XOF_PRF_OFFSET (SHAKE squeeze landing zone)
164224..262143      reserved (97920 bytes free)

POLY_SLOT_7 is reserved as scratch by polyvec_pointwise_acc_montgomery (the inner-product helper). Callers MUST NOT pass POLY_SLOT_7 as the result, matrix row, or vector argument to that function or to its caller polyvec_matrix_pointwise_montgomery.

MATRIX_SLOT is rounded up to 8 × 8 = 64 polys for clean addressing even though no parameter set uses k = ℓ = 8 simultaneously (ML-DSA-87 is k=8, ℓ=7 = 56 polys). Row-major: row i column j sits at MATRIX_SLOT + (i · ℓ + j) · 1024.


Module Identity

function getModuleId(): i32     // returns 6
function getMemoryPages(): i32  // returns 4

Memory Wiping

function wipeBuffers(): void

memory.fill(4096, 0, 160128), zeroes the entire mutable region in one pass. Covers all poly slots, the matrix slot, polyvec slots, byte buffers (seed/tr/μ/c̃/pk/sk/sig), and the XOF/PRF scratch.

The zetas data segment (offsets 0-4095) is not wiped. It is a compile-time constant.

The TypeScript wrapper calls wipeBuffers() in MlDsaBase.dispose(). Per-op secret residue is also wiped at the end of each public method; wipeBuffers() is the broader sweep at instance teardown.


Source Files

File Contents
buffers.ts Static buffer offsets, wipeBuffers, module identity getters.
params.ts Ring-level constants: Q, N, D, QINV, F_MONT, BARRETT_V. (k/ℓ/η/τ/etc. are TS-side.)
reduce.ts montgomery_reduce, barrett_reduce, fqmul, FIPS 204 Algorithm 49 + §2.3.
ntt.ts Scalar NTT/INTT (FIPS 204 Algorithms 41/42), zetas table, BitRev8.
ntt_simd.ts SIMD NTT/INTT, 4-lane i32 butterflies. Public ntt/invntt resolve to SIMD.
poly.ts Scalar poly_add/sub/reduce/caddq/pointwise_montgomery, poly_freeze, poly_chknorm, poly_tomont.
poly_simd.ts SIMD-vectorised counterparts. Public poly_add/sub/... resolve to SIMD.
polyvec.ts k-/ℓ-iterated polyvec wrappers + matrix-vector multiply + rounding wrappers + hint popcount.
encoding.ts simple_bit_pack/unpack (Alg 16/18), bit_pack/unpack (Alg 17/19), hint_bit_pack/unpack (Alg 20/21).
rounding.ts power2round, decompose, highbits, lowbits, make_hint, use_hint (FIPS 204 Algs 35-40).
sampling.ts rej_ntt_poly (Alg 30), rej_bounded_poly (Alg 31), sample_in_ball (Alg 29).
index.ts Public exports re-exposed from the files above.

API Reference

Buffer offset getters

getPolySlotBase, getPolySlotSize, getPolySlot0..7, getMatrixSlot, getMatrixSlotSize, getPolyvecSlotBase, getPolyvecSlotSize, getPolyvecSlot0..7, getSeedOffset, getTrOffset, getMsgRepOffset, getCTildeOffset, getPkOffset, getSkOffset, getSigOffset, getXofPrfOffset.

Reduction

function montgomery_reduce(a: i64): i32   // a · 2⁻³² mod q (centered)
function barrett_reduce(a: i32): i32       // centered Barrett, |output| < q/2
function fqmul(a: i32, b: i32): i32        // = montgomery_reduce(a · b)

NTT

function ntt(polyOff: i32): void           // FIPS 204 Algorithm 41 (SIMD)
function invntt(polyOff: i32): void        // FIPS 204 Algorithm 42 (SIMD)
function ntt_scalar(polyOff: i32): void    // scalar reference
function invntt_scalar(polyOff: i32): void // scalar reference
function getZeta(i: i32): i32              // zetas[i] (Montgomery form)
function getZetasOffset(): i32             // byte offset of zetas[0]
function BitRev8(m: i32): i32              // FIPS 204 Algorithm 43

Polynomial arithmetic

function poly_add(rOff, aOff, bOff): void                // c = a + b (SIMD)
function poly_sub(rOff, aOff, bOff): void                // c = a − b (SIMD)
function poly_reduce(polyOff): void                       // centered Barrett (SIMD)
function poly_caddq(polyOff): void                        // centered → [0, q-1] (SIMD)
function poly_pointwise_montgomery(rOff, aOff, bOff): void // c[i] = MR(a[i]·b[i]) (SIMD)
function poly_freeze(polyOff): void                       // canonicalise to [0, q-1]
function poly_chknorm(polyOff, bound): i32                // 1 iff |w_i| ≥ bound
function poly_tomont(polyOff): void                       // p[i] ← p[i]·R mod q

Scalar counterparts: poly_add_scalar, poly_sub_scalar, poly_reduce_scalar, poly_caddq_scalar, poly_pointwise_montgomery_scalar.

Polyvec wrappers

polyvec_add/sub/reduce/caddq/freeze/tomont/ntt/invntt, polyvec_pointwise_montgomery, polyvec_pointwise_acc_montgomery, polyvec_matrix_pointwise_montgomery, polyvec_chknorm, polyvec_power2round, polyvec_decompose, polyvec_highbits, polyvec_lowbits, polyvec_make_hint (returns popcount), polyvec_use_hint.

Encoding

function simple_bit_pack(rByteOff, polyOff, bitlen): void   // Alg 16
function bit_pack(rByteOff, polyOff, a, b): void             // Alg 17
function simple_bit_unpack(polyOff, vByteOff, bitlen): void  // Alg 18
function bit_unpack(polyOff, vByteOff, a, b): void           // Alg 19
function hint_bit_pack(rByteOff, hPvOff, k, omega): void     // Alg 20
function hint_bit_unpack(hPvOff, vByteOff, k, omega): i32    // Alg 21 (-1 on malformed)

hint_bit_unpack returns −1 on any of the three SUF-CMA-critical malformed-input cases (FIPS 204 §D.3 / Alg 21 lines 4, 9, 17).

Rounding

function power2round(r1Off, r0Off, aOff): void                  // Alg 35
function decompose(r1Off, r0Off, aOff, gamma2): void            // Alg 36
function highbits(rOff, aOff, gamma2): void                     // Alg 37
function lowbits(rOff, aOff, gamma2): void                      // Alg 38
function make_hint(hOff, zOff, rOff, gamma2): void              // Alg 39
function use_hint(rOff, hOff, aOff, gamma2): void               // Alg 40

All rounding kernels expect canonical-residue inputs in [0, q−1]. Apply polyvec_caddq (or poly_caddq) before calling these on any polyvec/poly that may carry centered residues.

Sampling

function rej_ntt_poly(polyOff, ctrStart, bufOff, bufLen): i32       // Alg 30
function rej_bounded_poly(polyOff, ctrStart, bufOff, bufLen, eta): i32 // Alg 31
function sample_in_ball(polyOff, signsOff, posBytesOff, posBytesLen, tau, startI): i32 // Alg 29

All three are resumable: callers feed XOF blocks one at a time and accumulate ctrStart until N=256 coefficients are accepted (or τ for sample_in_ball). The loop body is data-dependent on the public XOF output stream, see CT-posture notes.


Constant-time Posture

Coefficient arithmetic. montgomery_reduce, barrett_reduce, and fqmul are branch-free: only integer +, −, ×, and shift on input operands, no comparisons against secret values. The SIMD paths use v128 intrinsics with no lane-conditional operations.

Rejection sampling. rej_ntt_poly operates on bytes from the public seed ρ; data-dependent branching here leaks nothing about secret values (the matrix  is a public output of ExpandA). rej_bounded_poly operates on bytes from the secret seed ρ′, but the acceptance rate is uniform over each input byte regardless of seed value, the per-byte branch leaks only public information about ρ′-derived bytes the SHAKE output happens to land on. The Dilithium reference makes the same trade-off; FIPS 204 §7.3 endorses it.

SampleInBall. Branches on bytes derived from c̃, the signature commitment hash. c̃ is published in the signature and reconstructable by the verifier, branching here reveals only public information.

Norm check. poly_chknorm short-circuits on the first over-bound coefficient. The leak is the same observable rejection restart pattern that ML-DSA's signing loop already exposes: signer re-runs the loop on bound failure, attacker observes restart timing. Documented in source.

Decode. bit_unpack and simple_bit_unpack read public bytes linearly with no data-dependent branches.

Memory wipes. wipeBuffers() issues a single bulk memory.fill covering the entire mutable region.

All constant-time properties in this section are algorithm-level. See architecture.md §Where defense ends for the hardware-level disclaim.


Cross-References

Document Description
index Project Documentation index
architecture Repository structure, build and CI, WASM modules, public API, test suite, and security posture
mldsa.md MlDsa44, MlDsa65, MlDsa87: ML-DSA digital signatures (FIPS 204), pure mode and HashML-DSA
mldsa_audit.md ML-DSA FIPS 204 prehashed-input surface audit
asm_imports.md Per-module AssemblyScript import dependency graphs

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