Pure-Go ML-DSA-65 (NIST FIPS 204, the standardized Dilithium) — a post-quantum lattice signature scheme, implemented for readability, with zero dependencies beyond the Go standard library.
⚠️ Educational implementation. Conformant and cross-checked (below), but not constant-time, side-channel audited, or FIPS validated. For production signing use an audited implementation —crypto/mldsawhen the Go standard library ships it, or Cloudflare CIRCL today. This package is for studying how a lattice signature actually works, with a debugger in hand. Start with the introduction — it's the fun version.
Ed25519's security rests on discrete logs, which a large quantum computer breaks. The standardized post-quantum replacement rests on lattices — finding short vectors in thousands of dimensions — and reads like a different world: Fiat–Shamir with aborts, rejection sampling as a security mechanism, hints that patch rounding scars. The audited implementations are (rightly) optimized and hardened, which makes them hard to learn from. This one is the readable version: plain arithmetic, derived constants, spec names, and comments that explain why each rejection bound exists.
For a battery of seeds, this implementation and Cloudflare CIRCL's ML-DSA-65
produce byte-identical key pairs and byte-identical deterministic
signatures, and signatures cross-verify in both directions. Property tests
pin the internals: NTT multiplication against schoolbook negacyclic
convolution, the hint-recovery lemma over 500k inputs, canonical-encoding
strictness, and every codec round-trip. go test -race clean.
import "github.com/netstar-labs/lattice"
priv, _ := lattice.GenerateKey() // or NewKeyFromSeed([32]byte{...})
pub := priv.PublicKey()
sig, _ := priv.Sign(msg, nil) // hedged (default; nil = no context)
sig, _ = priv.SignDeterministic(msg, ctx) // reproducible; ctx ≤ 255 bytes
ok := pub.Verify(sig, msg, nil)
// serialize / parse (FIPS 204 encodings)
pk := pub.Bytes() // 1952 bytes
sk := priv.Bytes() // 4032 bytes
pub2, _ := lattice.ParsePublicKey(pk)
priv2, _ := lattice.ParsePrivateKey(sk) // integrity-checked against trA runnable demo lives in example/: go run ./example.
| bytes | Apple M2 Pro | ||
|---|---|---|---|
| public key | 1,952 | keygen | ~242 µs |
| private key | 4,032 | sign | ~385 µs (≈4 rejection passes avg) |
| signature | 3,309 | verify | ~61 µs |
(Ed25519 for scale: 32 / 64 / 64 bytes. Post-quantum is measured in bytes.)
| path | contents |
|---|---|
params.go |
the ML-DSA-65 constants (FIPS 204 Table 1) |
field.go |
mod-q arithmetic, rounding, hints (§7.4) |
ntt.go |
the number-theoretic transform, twiddles derived at init (§7.5) |
poly.go |
polynomials and module vectors (§6) |
sample.go |
the four SHAKE samplers (§7.3) |
pack.go |
bit codecs and the canonical hint encoding (§7.1) |
lattice.go |
keys, sign, verify (§5–6) |
docs/ |
introduction · architecture |
GPL-3.0-or-later. See LICENSE.