Skip to content

x.crypto.mldsa #

mldsa

Pure V implementation of ML-DSA (FIPS 204), a post-quantum digital signature algorithm. Supports all three parameter sets (ML-DSA-44, ML-DSA-65, ML-DSA-87).

This is still experimental > It is verified against NIST ACVP test vectors for keygen, > signing, and verification, > but not yet production-ready.

Example

import x.crypto.mldsa

fn main() {
    // generate a new ML-DSA-65 key pair
    sk := mldsa.PrivateKey.generate(.ml_dsa_65)!
    pk := sk.public_key()

    // sign a message (with an optional context string)
    msg := 'Hello ML-DSA'.bytes()
    sig := sk.sign(msg, context: 'not-a-drill')!

    // verify the signature with the same context
    verified := pk.verify(msg, sig, context: 'not-a-drill')!
    assert verified // true

    // deterministic signing is also available
    sig2 := sk.sign(msg, context: 'not-a-drill', deterministic: true)!
    verified2 := pk.verify(msg, sig2, context: 'not-a-drill')!
    assert verified2 // true
}

Constants #

const seed_size = 32
const public_key_size_44 = 32 + 4 * n * 10 / 8

s. 4, table 2

const public_key_size_65 = 32 + 6 * n * 10 / 8
const public_key_size_87 = 32 + 8 * n * 10 / 8
const signature_size_44 = 128 / 4 + 4 * n * (17 + 1) / 8 + 80 + 4

s. 4, table 2

const signature_size_65 = 192 / 4 + 5 * n * (19 + 1) / 8 + 55 + 6
const signature_size_87 = 256 / 4 + 7 * n * (19 + 1) / 8 + 75 + 8

fn compute_mu #

fn compute_mu(tr []u8, msg []u8, context string) [64]u8

algo. 2, lines 10-11: M' = 0x00 || |ctx| || ctx || M; mu = H(tr || M', 64) compute_mu computes mu = H(tr || M', 64) where M' = 0x00 || |ctx| || ctx || msg.

fn compute_mu_prehash #

fn compute_mu_prehash(tr []u8, msg []u8, context string, ph PreHash) [64]u8

algo. 4, line 23: M' = 0x01 || |ctx| || ctx || OID || PH(M) algo. 7, line 6: mu = H(tr || M') compute_mu_prehash computes mu for prehash mode: H(tr || 0x01 || |ctx| || ctx || OID || PH(msg), 64).

fn PrivateKey.from_bytes #

fn PrivateKey.from_bytes(raw []u8, kind Kind) !PrivateKey

from FIPS 204 semi-expanded encoding. seed() and equal() are meaningless on the result — use from_seed when possible.

fn PrivateKey.from_seed #

fn PrivateKey.from_seed(seed []u8, kind Kind) !PrivateKey

fn PrivateKey.generate #

fn PrivateKey.generate(kind Kind) !PrivateKey

algo. 1: ML-DSA.KeyGen (s. 5.1)

fn PublicKey.from_bytes #

fn PublicKey.from_bytes(raw []u8, kind Kind) !PublicKey

enum Kind #

enum Kind {
	ml_dsa_44
	ml_dsa_65
	ml_dsa_87
}

s. 4, table 1

fn (Kind) public_key_size #

fn (k Kind) public_key_size() int

fn (Kind) private_key_size #

fn (k Kind) private_key_size() int

fn (Kind) signature_size #

fn (k Kind) signature_size() int

enum PreHash #

enum PreHash {
	none // pure ML-DSA (default)
	sha2_224
	sha2_256
	sha2_384
	sha2_512
	sha2_512_224
	sha2_512_256
	sha3_224
	sha3_256
	sha3_384
	sha3_512
	shake_128
	shake_256
}

FIPS 204 s. 5.4: approved pre-hash functions for HashML-DSA.

struct PrivateKey #

struct PrivateKey {
	seed [32]u8
	pk   PublicKey
	s1   []NttElement // len = l
	s2   []NttElement // len = k
	t0   []NttElement // len = k
	k    [32]u8
}

fn (PrivateKey) public_key #

fn (sk &PrivateKey) public_key() &PublicKey

fn (PrivateKey) seed #

fn (sk &PrivateKey) seed() []u8

fn (PrivateKey) bytes #

fn (sk &PrivateKey) bytes() []u8

fn (PrivateKey) equal #

fn (sk &PrivateKey) equal(other &PrivateKey) bool

seed-based constant-time comparison. not meaningful for from_bytes keys.

fn (PrivateKey) equal_bytes #

fn (sk &PrivateKey) equal_bytes(other &PrivateKey) bool

constant-time comparison of the serialized key material. slower but works for from_bytes keys.

fn (PrivateKey) sign #

fn (sk &PrivateKey) sign(msg []u8, opts SignerOpts) ![]u8

algo. 2/4: ML-DSA.Sign / HashML-DSA.Sign (s. 5.2, 5.4.1)

fn (PrivateKey) sign_mu #

fn (sk &PrivateKey) sign_mu(mu []u8, rnd []u8) ![]u8

sign_mu signs a precomputed mu value with explicit randomness. mu must be 64 bytes. rnd must be 32 bytes (use all zeros for deterministic signing).

struct PublicKey #

struct PublicKey {
	raw []u8
	p   Params
	a   []NttElement // k*l matrix in NTT domain
	t1  []NttElement // len = k, NTT(t1 * 2^d)
	tr  [64]u8
}

fn (PublicKey) bytes #

fn (pk &PublicKey) bytes() []u8

fn (PublicKey) tr #

fn (pk &PublicKey) tr() []u8

tr returns the 64-byte transcript hash (H(pk)) used in mu computation.

fn (PublicKey) equal #

fn (pk &PublicKey) equal(other &PublicKey) bool

fn (PublicKey) verify #

fn (pk &PublicKey) verify(msg []u8, sig []u8, opts SignerOpts) !bool

algo. 3/5: ML-DSA.Verify / HashML-DSA.Verify (s. 5.3, 5.4.1)

fn (PublicKey) verify_mu #

fn (pk &PublicKey) verify_mu(mu []u8, sig []u8) !bool

struct SignerOpts #

@[params]
struct SignerOpts {
pub:
	context       string
	deterministic bool
	prehash       PreHash
}