Skip to content

itzmeanjan/ml-dsa

Repository files navigation

ML-DSA (formerly known as Dilithium)

NIST FIPS 204 (ML-DSA) standard compliant, C++20, fully constexpr, header-only library implementation. FIPS 204 compliance is assured by testing this implementation against NIST ACVP Known Answer Tests and tons of property based tests. We also fuzz the library and its internal components with LLVM libFuzzer to get an extra level of assurance. Fuzzing is still work in progress.

Note

constexpr? Yes, you can compile-time execute keygen, sign and verify.

Caution

This ML-DSA implementation is conformant with ML-DSA standard https://doi.org/10.6028/NIST.FIPS.204 and I also try to make it timing leakage free, but be informed that this implementation is not yet audited. If you consider using it in production, please be careful !

Quick Start

Add ml-dsa to your CMake project via FetchContent:

include(FetchContent)
FetchContent_Declare(
  ml-dsa
  GIT_REPOSITORY https://github.com/itzmeanjan/ml-dsa.git
  GIT_TAG master
  GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ml-dsa)

target_link_libraries(my_app PRIVATE ml-dsa)

Generate a keypair, sign a message, and verify the signature:

#include "ml_dsa/ml_dsa_65.hpp"
#include "randomshake/randomshake.hpp"

// Generate a keypair
std::array<uint8_t, ml_dsa_65::KeygenSeedByteLen> seed{};
std::array<uint8_t, ml_dsa_65::PubKeyByteLen> pubkey{};
std::array<uint8_t, ml_dsa_65::SecKeyByteLen> seckey{};

randomshake::randomshake_t csprng;
csprng.generate(seed);
ml_dsa_65::keygen(seed, pubkey, seckey);

// Sign a message (hedged mode)
std::array<uint8_t, ml_dsa_65::SigningSeedByteLen> rnd{};
std::array<uint8_t, ml_dsa_65::SigByteLen> sig{};
csprng.generate(rnd);

std::vector<uint8_t> msg = { /* your message bytes */ };
const bool signed_ok = ml_dsa_65::sign(rnd, seckey, msg, {}, sig);

// Verify
const bool valid = ml_dsa_65::verify(pubkey, msg, {}, sig);

See examples/ for a complete standalone CMake project.

Motivation

ML-DSA has been standardized by NIST as post-quantum secure digital signature algorithm (DSA), which can be used for verifying the authenticity of digital messages, giving the recipient confidence that the message indeed came from the known sender.

ML-DSA's security is based on the hardness of finding short vectors in module (i.e., structured) lattices. It is built on the "Fiat-Shamir with Aborts" paradigm, where the signing procedure might abort and restart multiple times based on the message, context string and randomness.

Algorithm Input Output What does it do ?
KeyGen 32 -bytes seed Public and Secret Key 32 -bytes seed is used for deterministically deriving a ML-DSA keypair.
Sign 32 -bytes seed, secret key, message, context Signature Signs a message with an optional context string (max 255 -bytes). Hedged (randomized) signing is default and recommended; for deterministic signing, fill seed with zero bytes.
Verify Public key, message, context, signature Boolean Verifies a signature against a message and optional context.
HashSign 32 -bytes seed, secret key, message, context Signature Pre-hash mode signing (FIPS 204 §5.4). Pre-hashes the message with a SHA3 hash function before signing. Same hedged/deterministic modes as Sign.
HashVerify Public key, message, context, signature Boolean Pre-hash mode verification (FIPS 204 §5.5). Verifies a signature against the pre-hashed representation of a message.
Sign_internal 32 -bytes seed, secret key, M' Signature FIPS 204 Algorithm 7. Signs using the encoded message M' directly (bypassing context encoding).
Verify_internal Public key, M', signature Boolean FIPS 204 Algorithm 8. Verifies using the encoded message M' directly.
Sign_core 32 -bytes seed, secret key, 64 -bytes mu Signature Core signing operation. Accepts the 64 -bytes message representative mu directly. Caller is responsible for computing mu.
Verify_core Public key, 64 -bytes mu, signature Boolean Core verification operation. Accepts the 64 -bytes message representative mu directly.

Tip

API tiers: Most users should use the standard API -- keygen, sign, verify, hash_sign, hash_verify. The advanced API (sign_internal, verify_internal, sign_core, verify_core) is for implementors who has direct access to the encoded message M' or the 64-byte message representative mu.

Note

Return values: sign() and hash_sign() return false only if the context string exceeds 255 bytes. The internal rejection-sampling loop runs until a valid signature is produced -- it does not abort to the caller. In normal usage with valid inputs, signing always succeeds. verify() and hash_verify() return false if the context exceeds 255 bytes, or if the signature is invalid (malformed hint bits, norm check failure, or commitment mismatch).

Note

Context strings (ctx): The context parameter is a domain-separation mechanism (FIPS 204 §5.2) that allows the same key to be safely used across different application protocols without cross-protocol attacks. The context used at signing time must match at verification time. Pass an empty span ({}) if you don't need domain separation. Maximum length: 255 bytes.

Here I'm maintaining ml-dsa - a C++20 header-only fully constexpr library, implementing ML-DSA, supporting ML-DSA-{44, 65, 87} parameter sets, as defined in table 1 of ML-DSA standard. It's easy to use, see usage.

ML-DSA-65 Algorithm Time taken on "12th Gen Intel(R) Core(TM) i7-1260P" (x86_64) Time taken on "AWS EC2 Instance c8g.large, featuring ARM Neoverse-V2" (aarch64)
keygen 82.1us 126.2us
sign 587us 879us
verify 86.3us 134.4us

Note

All numbers in the table above represent the median time required to execute a specific algorithm, except for signing. In the case of signing, the number represents the minimum time required to sign a 32 -bytes message. Find more details in benchmarking.

Note

Find ML-DSA standard @ https://doi.org/10.6028/NIST.FIPS.204 - this is the document that I followed when implementing ML-DSA. I suggest you go through the specification to get an in-depth understanding of the scheme.

Security & Robustness

This implementation is built with a "Security-First" approach, incorporating programming language features and multiple tools for enforcement:

  • No Raw Pointers: Completely avoids dealing with raw pointers. Everything is wrapped in statically-sized std::span, which provides much better type safety and compile-time error reporting as the exact byte lengths for all buffers (seeds, keys, signatures) are known for each parameter set.
  • Strict Build Guards: Compiled with -Werror and comprehensive warning flags (-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wformat=2 -Wcast-qual -Wold-style-cast -Wundef). Any warnings triggered during compilation are treated as fatal errors.
  • Memory Safety: Verified using AddressSanitizer (ASan) in both release and debug build configuration.
  • Undefined Behavior: Hardened with UndefinedBehaviorSanitizer (UBSan), configured to treat any undefined behavior as a fatal, non-recoverable error.
  • Continuous Fuzzing: Includes a suite of 13 specialized fuzzer binaries (9 signature variants + 4 internal component units).
  • Static Analysis: Integrated with clang-tidy using an extensive check suite (bugprone-*, cert-*, clang-analyzer-*, concurrency-*, cppcoreguidelines-*, hicpp-*, misc-*, modernize-*, performance-*, portability-*, readability-*) with all warnings treated as errors.
  • CI-Verified: Automatically tested on every push across a matrix of operating systems (Linux, macOS) and compilers (clang++, g++).

Prerequisites

  • A C++ compiler such as clang++/ g++, with support for compiling C++20 programs.
  • CMake 3.28 or later.
  • For testing, google-test is required. It can be installed globally or fetched automatically by setting -DML_DSA_FETCH_DEPS=ON.
  • For benchmarking, google-benchmark is required. It can be installed globally or fetched automatically by setting -DML_DSA_FETCH_DEPS=ON.
  • For static analysis, you'll need clang-tidy.
  • For code formatting, you'll need clang-format.

Automatically Fetched Dependencies

The following libraries are automatically fetched by CMake at configure time -- no manual installation needed:

  • sha3: SHA3 suite of hash functions, from FIPS 202, used internally by ML-DSA.
  • randomshake: A CSPRNG built on (Turbo)SHAKE256 XOF, seeded from OS entropy. Used in tests, benchmarks and examples for generating keygen seeds and signing randomness. You may use any cryptographically secure pseudo-random source to fill the seed/rnd arrays; the library does not mandate a specific RNG.

Note

If you are on a machine running GNU/Linux kernel and you want to obtain CPU cycle count for ML-DSA routines, you should consider building google-benchmark library with libPFM support, following https://gist.github.com/itzmeanjan/05dc3e946f635d00c5e0b21aae6203a7, a step-by-step guide. Find more about libPFM @ https://perfmon2.sourceforge.net. When libpfm is installed, CMake will automatically detect and link it.

Building

For testing functional correctness of this implementation and conformance with ML-DSA standard, you have to run following command(s).

Note

All Known Answer Test (KAT) files live inside kats directory. KAT files from official reference implementation, are generated by following (reproducible) steps, described in https://gist.github.com/itzmeanjan/d14afc3866b82119221682f0f3c9822d. ACVP KATs can be synced by building sync_acvp_kats target.

CMake Options

Option Description Default
ML_DSA_BUILD_TESTS Build test suite OFF
ML_DSA_BUILD_BENCHMARKS Build benchmarks OFF
ML_DSA_BUILD_EXAMPLES Build examples OFF
ML_DSA_BUILD_FUZZERS Build fuzzers (requires Clang) OFF
ML_DSA_FETCH_DEPS Fetch missing dependencies (Google Test, Google Benchmark) OFF
ML_DSA_ASAN Enable AddressSanitizer OFF
ML_DSA_UBSAN Enable UndefinedBehaviorSanitizer OFF
ML_DSA_NATIVE_OPT Enable -march=native (not safe for cross-compilation) OFF
ML_DSA_ENABLE_LTO Enable Interprocedural Optimization (LTO) ON

Tip

If you are building for the same machine that will run the code (i.e., cross-compilation is not the goal), you should enable -DML_DSA_NATIVE_OPT=ON to allow the compiler to unroll loops and auto-vectorize, using processor-specific optimizations (like AVX2, NEON, etc.) for maximum performance.

Testing

To build and run the tests, use the following CMake commands:

cmake -B build -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_TESTS=ON -DML_DSA_FETCH_DEPS=ON
cmake --build build -j

ctest --test-dir build --output-on-failure -j

To run tests with sanitizers, reconfigure the build with the desired sanitizer option:

# With AddressSanitizer, in Release mode
cmake -B build -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_TESTS=ON -DML_DSA_FETCH_DEPS=ON -DML_DSA_ASAN=ON
cmake --build build -j && ctest --test-dir build --output-on-failure -j

# With UndefinedBehaviorSanitizer, in Release mode
cmake -B build -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_TESTS=ON -DML_DSA_FETCH_DEPS=ON -DML_DSA_UBSAN=ON
cmake --build build -j && ctest --test-dir build --output-on-failure -j

Benchmarking

To run the benchmarks (using Google Benchmark):

cmake -B build -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_BENCHMARKS=ON -DML_DSA_FETCH_DEPS=ON -DML_DSA_NATIVE_OPT=ON
cmake --build build -j

Caution

When benchmarking, ensure that you've disabled CPU frequency scaling, by following guide @ https://github.com/google/benchmark/blob/main/docs/reducing_variance.md.

Warning

Relying only on average timing measurement for understanding performance characteristics of ML-DSA sign algorithm may not be a good idea, given that it's a post-quantum digital signature scheme of "Fiat-Shamir with Aborts" paradigm - simply put, during signing procedure it may need to abort and restart again, multiple times, based on what message is being signed or what random seed is being used for default hedged signing. So it's a better idea to also compute other statistics such as minimum, maximum and median when timing execution of sign procedure. In following benchmark results, you'll see such statistics demonstrating broader performance characteristics of ML-DSA sign procedure for various parameter sets. Also to easily compare performance benchmarking result of sign function across different configurations, we use a fixed seed to initialize RandomShake CSPRNG. This gives us deterministic result.

# In case it linked with libPFM, you can get CPU cycle count
./build/ml_dsa_benchmarks --benchmark_time_unit=us --benchmark_min_warmup_time=.5 --benchmark_enable_random_interleaving=true --benchmark_repetitions=10 --benchmark_min_time=0.1s --benchmark_display_aggregates_only=true --benchmark_report_aggregates_only=true --benchmark_counters_tabular=true --benchmark_perf_counters=CYCLES

# Otherwise, you can get time taken in micro-seconds
./build/ml_dsa_benchmarks --benchmark_time_unit=us --benchmark_min_warmup_time=.5 --benchmark_enable_random_interleaving=true --benchmark_repetitions=10 --benchmark_min_time=0.1s --benchmark_display_aggregates_only=true --benchmark_report_aggregates_only=true --benchmark_counters_tabular=true

Fuzzing

This project includes 13 specialized fuzzer binaries built with LLVM libFuzzer. Each fuzzer has its own isolated corpus directory and tuned input sizes for maximum coverage.

Recommended: Run All Fuzzers

The easiest way to fuzz is with the full-lifecycle script. It configures, builds, generates per-fuzzer seed corpus with correctly-sized seed files, runs all 13 fuzzers in parallel, and produces a summary report:

# Default: 1 hour per fuzzer, using all CPU cores
./scripts/fuzz_all.sh

# Quick smoke test (2 minutes)
FUZZ_TIME=120 ./scripts/fuzz_all.sh

# Customize parallelism
FUZZ_JOBS=4 FUZZ_FORK=2 ./scripts/fuzz_all.sh
Variable Description Default
FUZZ_TIME Seconds to run each fuzzer 3600 (1 hour)
FUZZ_JOBS Max concurrent fuzzer processes 4
FUZZ_FORK Fork workers per fuzzer (nproc / FUZZ_JOBS) - 1
CXX C++ compiler (must be Clang) clang++
BUILD_DIR Build output directory build
CORPUS_DIR Persistent corpus directory fuzz_corpus

After completion, the script prints a report showing corpus size, total executions, and status per fuzzer. Log files are saved to fuzz_report/.

fuzzer-1h-run-report

Manual Single-Fuzzer Run

To run a specific fuzzer manually (requires clang++):

cmake -B build -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_FUZZERS=ON
cmake --build build -j

# Create corpus with correctly-sized seed
mkdir -p fuzz_corpus/ml_dsa_65_sign
head -c 4129 /dev/urandom > fuzz_corpus/ml_dsa_65_sign/seed

./build/ml_dsa_65_sign_fuzzer fuzz_corpus/ml_dsa_65_sign \
  -max_total_time=3600 \
  -max_len=4129 \
  -fork=$(nproc) \
  -print_final_stats=1 \
  -print_corpus_stats=1

Important

Each fuzzer requires a specific minimum input size. Using incorrect sizes wastes mutation cycles. Use -max_len matching the size from the table below.

Input Size Reference

Each fuzzer has a minimum input size determined by its input requirements. Use these as -max_len values:

Fuzzer -max_len Composition
ml_dsa_44_keygen 32 seed(32)
ml_dsa_65_keygen 32 seed(32)
ml_dsa_87_keygen 32 seed(32)
ml_dsa_44_sign 2657 seckey(2560) + rnd(32) + msg(32) + ctx(1) + seed(32)
ml_dsa_65_sign 4129 seckey(4032) + rnd(32) + msg(32) + ctx(1) + seed(32)
ml_dsa_87_sign 4993 seckey(4896) + rnd(32) + msg(32) + ctx(1) + seed(32)
ml_dsa_44_verify 3797 pubkey(1312) + sig(2420) + msg(32) + ctx(1) + seed(32)
ml_dsa_65_verify 5326 pubkey(1952) + sig(3309) + msg(32) + ctx(1) + seed(32)
ml_dsa_87_verify 7348 pubkey(2592) + sig(4627) + msg(32) + ctx(1) + seed(32) + extra(64)
field_arithmetic 8 2 × uint32
poly_ntt 1024 256 × uint32
poly_sampling 65 1B selector + 64B sampling input
poly_bit_packing 641 1B selector + 640B packing input

Integration

You can easily integrate ml-dsa into your project using CMake.

# Install system-wide (default prefix: /usr/local)
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
sudo cmake --install build

# Install to custom directory (e.g., ./dist)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./dist
cmake --build build
cmake --install build

Or using FetchContent in your CMakeLists.txt:

include(FetchContent)
FetchContent_Declare(
  ml-dsa
  GIT_REPOSITORY https://github.com/itzmeanjan/ml-dsa.git
  GIT_TAG master
  GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ml-dsa)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE ml-dsa)

Development Tools

# Configure
cmake -B build -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_TESTS=ON -DML_DSA_BUILD_EXAMPLES=ON -DML_DSA_FETCH_DEPS=ON

# Static analysis (requires clang-tidy)
cmake --build build --target tidy

# Format source code (requires clang-format)
cmake --build build --target format

# Sync ACVP Known Answer Test vectors
cmake --build build --target sync_acvp_kats

Usage

ml-dsa is written as a header-only C++20 fully constexpr library, mainly targeting 64 -bit mobile/ desktop/ server grade platforms and it's easy to get started with. All you need to do is following.

  • Clone ml-dsa repository.
git clone https://github.com/itzmeanjan/ml-dsa.git && cd ml-dsa

cmake -B build -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_TESTS=ON -DML_DSA_FETCH_DEPS=ON
cmake --build build -j
ctest --test-dir build --output-on-failure -j
  • Write your program; include proper header files ( based on which variant of ML-DSA you want to use, see include directory ), which includes declarations ( and definitions ) of all required ML-DSA routines and constants ( such as byte length of public/ private key, signature etc. ).
// main.cpp

#include "ml_dsa/ml_dsa_44.hpp"
#include "randomshake/randomshake.hpp"
#include <cassert>
#include <iostream>

int main() {
  std::array<uint8_t, ml_dsa_44::KeygenSeedByteLen> seed{};
  std::array<uint8_t, ml_dsa_44::PubKeyByteLen> pubkey{};
  std::array<uint8_t, ml_dsa_44::SecKeyByteLen> seckey{};

  randomshake::randomshake_t csprng;
  csprng.generate(seed);

  ml_dsa_44::keygen(seed, pubkey, seckey);

  std::array<uint8_t, ml_dsa_44::SigningSeedByteLen> rnd{};
  std::array<uint8_t, ml_dsa_44::SigByteLen> sig{};
  csprng.generate(rnd);

  std::vector<uint8_t> msg(32, 0);
  csprng.generate(msg);

  const bool has_signed = ml_dsa_44::sign(rnd, seckey, msg, {}, sig);
  const bool is_valid = ml_dsa_44::verify(pubkey, msg, {}, sig);

  assert(has_signed && is_valid);
  std::cout << "Verified: " << std::boolalpha << is_valid << "\n";

  return 0;
}
  • If your project uses CMake, the recommended approach is to use find_package or FetchContent (see Integration section above).

Note

Randomness: The examples use randomshake::randomshake_t, a CSPRNG seeded from OS entropy, to generate keygen seeds and signing randomness. You may use any cryptographically secure pseudo-random source to fill the seed and rnd arrays -- the library does not mandate a specific RNG. For deterministic signing, fill rnd with zero bytes. Though that is not the recommended way of using ML-DSA signing.

Tip

Add -march=native to optimize for your specific CPU. Omit it if building for distribution or cross-compilation.

Pre-Hash Mode (HashSign / HashVerify)

For signing pre-hashed messages (FIPS 204 §5.4-5.5), use hash_sign and hash_verify with a hash_algorithm template parameter:

#include "ml_dsa/ml_dsa_65.hpp"
using ml_dsa_65::hash_algorithm;

// Sign a pre-hashed message using SHA3-256
const bool ok = ml_dsa_65::hash_sign<hash_algorithm::SHA3_256>(rnd, seckey, msg, ctx, sig);
// Verify
const bool valid = ml_dsa_65::hash_verify<hash_algorithm::SHA3_256>(pubkey, msg, ctx, sig);

Supported hash_algorithm values: SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE_128, SHAKE_256. We do not support using SHA2 hash functions for pre-hashing.

Choosing a Parameter Set

Variant NIST Security Level Public Key Secret Key Signature Namespace Header
ML-DSA-44 2 (comparable to AES-128) 1,312 B 2,560 B 2,420 B ml_dsa_44:: include/ml_dsa/ml_dsa_44.hpp
ML-DSA-65 3 (comparable to AES-192) 1,952 B 4,032 B 3,309 B ml_dsa_65:: include/ml_dsa/ml_dsa_65.hpp
ML-DSA-87 5 (comparable to AES-256) 2,592 B 4,896 B 4,627 B ml_dsa_87:: include/ml_dsa/ml_dsa_87.hpp

ML-DSA-65 is recommended for general use. Choose ML-DSA-44 when signature/key size is critical, or ML-DSA-87 when you need the highest security margin.

Note

ML-DSA parameter sets are taken from table 1 of ML-DSA standard @ https://doi.org/10.6028/NIST.FIPS.204.

All the functions, in this ML-DSA header-only library, are implemented as constexpr functions. Hence you should be able to evaluate ML-DSA key generation, signing or verification at compile-time itself, given that all inputs are known at compile-time. I present you with the following demonstration program, which generates a ML-DSA-44 keypair, signs a message, and verifies the signature - all at program compile-time. Notice, the static assertion.

/**
 * Filename: compile-time-ml-dsa-44.cpp
 *
 * Compile and run this program with
 * Build and run using the CMake example project in examples/ directory, or
 * use `cmake --install` to install ml-dsa system-wide and compile with:
 * $ g++ -std=c++20 -Wall -Wextra -Wpedantic -fconstexpr-ops-limit=500000000 compile-time-ml-dsa-44.cpp && ./a.out
 * $ clang++ -std=c++20 -Wall -Wextra -Wpedantic -fconstexpr-steps=500000000 compile-time-ml-dsa-44.cpp && ./a.out
 */

#include "ml_dsa/ml_dsa_44.hpp"
#include <string_view>

// Compile-time hex character to nibble conversion.
constexpr uint8_t
hex_digit(char c)
{
  if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0');
  if (c >= 'a' && c <= 'f') return static_cast<uint8_t>(c - 'a' + 10);
  if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
  return 0;
}

// Compile-time hex string to byte array conversion.
template<size_t L>
constexpr std::array<uint8_t, L>
from_hex(std::string_view str)
{
  std::array<uint8_t, L> res{};
  for (size_t i = 0; i < L; i++) {
    res[i] = static_cast<uint8_t>((hex_digit(str[2 * i]) << 4) | hex_digit(str[(2 * i) + 1]));
  }
  return res;
}

// Compile-time evaluation of ML-DSA-44 keygen, signing and verification.
constexpr bool
eval_ml_dsa_44()
{
  constexpr auto seed = from_hex<32>("7c9935a0b07694aa0c6d10e4db6b1add2fd81a25ccb148032dcd739936737f2d");
  constexpr auto msg = from_hex<33>("d81c4d8d734fcbfbeade3d3f8a039faa2a2c9957e835ad55b22e75bf57bb556ac8");
  constexpr auto ctx = from_hex<33>("8626ed79d451140800e03b59b956f8210e556067407d13dc90fa9e8b872bfb8fab");
  constexpr auto rnd = from_hex<32>("6255563ba961772146ca0867678d56787cad77ab4fc8fcfe9e02df839c99424d");

  std::array<uint8_t, ml_dsa_44::PubKeyByteLen> pubkey{};
  std::array<uint8_t, ml_dsa_44::SecKeyByteLen> seckey{};
  std::array<uint8_t, ml_dsa_44::SigByteLen> sig{};

  ml_dsa_44::keygen(seed, pubkey, seckey);
  const auto signed_ok = ml_dsa_44::sign(rnd, seckey, msg, ctx, sig);
  const auto verified = ml_dsa_44::verify(pubkey, msg, ctx, sig);

  return signed_ok && verified;
}

int
main()
{
  // Entire ML-DSA-44 keygen + sign + verify round-trip, evaluated at compile-time.
  static_assert(eval_ml_dsa_44(), "ML-DSA-44 keygen/sign/verify must be correct at compile-time");
  return 0;
}

See examples/README.md for a standalone CMake project and detailed documentation on how to integrate and use ml-dsa in your own project. It's the quickest way to get started:

# Build the example as a standalone project
cd examples
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/ml_dsa_44

# Or build from the repository root
cmake -B build -DCMAKE_BUILD_TYPE=Release -DML_DSA_BUILD_EXAMPLES=ON
cmake --build build -j
./build/ml_dsa_44
ML-DSA-44 @ NIST security level 2
Seed      : e4196b0fd4bb4f0721860fe6f83184180276d04f5017bb971d09bbbaac70c223
Pubkey    : e3f37358b35bb4b7807acf7bea046db0a30fc74c6926100e5230d2e7c562e4eb762370fffac83fc2a72efb24c10a1119c4edd224cc29d7e042b53d6b1b2c08ef71c0e4b3b36df428976ffabd3e077643437666367f3e64b36d65c10315e57de2f12bc21b7ff144bfcd50eb1bf7967b58d859e3cc2f278d3ff4ea94b69ca1eaa804b097cfbd3d8a36860f335f17fce76cce005d608f8f1baac1f27e4b39adb201944c1f0c31026787f393581248585caed04c85977d4cd94ef1ae00dfff33eaf6750d455157146c91b7d64bcbaacb76ae28999e84cd05f15f796dae6c67d682d86bd0b7e09a185f8bccb480a32693abe9af5744913084f459b1887e018c8c267b02864c36e41fd7845f6fa3bb73201aa10ce2cb9b743e6c5c2e90c9c2dc59de89ccece4f66078fc74eb40fe2f86d65faed5abe4ec862fb7f04ba0775f4dd9ecfa18dcab6ad867e067bcd2c101869f81e2967da02d8b832769a558f6d04e0048b02e20dc126f90dd1323649a6b4c25ad0d54e0c99e35945febe89a80ebeb55416c66243d5900557a4c0b981fb297997e8ae7cefb1efdb6f5dee4a931995c7d702ef2849434c3c193a12d8da31e30d8c774f4c0c5a0b1cbc80a9cd282c7cc746e4e0aebfbbf56d25cb4ebade59fc23e1a0dba83608753167adb80cce41d9f14e6c2e2dd9cdd54887058e68fb838903f17571650b2555eb45192c7a952f8cb20de67655c2bfa8181a8dc72bb962192904cd29cc3275f630b1e5826f0117a83262cf275f4a3f484e1d6336d63ffa0e5dc03b3aa508e6e1f688b9fe72dc46c1453a901e4233c237497f97d5cdd0484ccd1cc983b465e69c2e0e40d0183f386b5862ca14487b3423292455748deb5bdcef13c18f0510613468825603d71712393fff83cbe7414c92670379f9eccee1c95f433c73c09cd3da1bf3c67ec31d0f5a67f18d01347cd3684a27f196fe67d49b4a95b5caa2fbdd8c12050771276d5637537b9f7407a7f9b0f9326ca292be99392d0c91657e74fc63f96f968724be3959ae459e8f1bcbf10583655eb0bf2dbbe4eab80701853ee112a5ce42efbe2587ca323585bb023d862e4b5f8f8c7a916bad7a36710d1cd06c0948f83e7abbe7cef2a1de82d9e55e003dd31433214f5ae5937253176a1a95ccb4e12489c290839013e33df62ce5d08e36338ae4e6b707f570ab1829ed3c944a643f4e8d485e93379ed1def902d25c0adcb517e10e11a2f1bfee1754162b82a116e2bd52ec8869ce1c5037bc0a2e3cf6be290189a8c6c73835890a302ed40c4d2206dd06a5db31c87349852e94aa78506c16224f0d0ba8e9cfd3579e12c01664c51799108653b58c9e4814ef4257e446a42632e939577cc6a5cf9e4313053343bde753be7911dd8ce70a66c5cd529d08e021e1cbeb67ead34abd0b250f2901b0710288e4310796444eb98d9aa79012690b5b674791b3ebeb162d5321b63b99bb1e4b504e58cf47452e9ea180d695a94a75817b338daaa032ae51bf5c6a2cb69ea51d075c6d4fa112120b9ee44e369e5bb39edd29f37ce30e800eeaf05d6abfa8225441cfd2224bac608fcab3441a40601714a7418a717ad130f850243ffdde1836650a3c24cd9d74efd0e2f37b7e3c30d0d161f980d5a11c1f46948d59e68f35b0449ad07fd0c395a8af7f7ec5eae43566eb52b3180561475df1272a32295b2b8ef614e47d45ffeaa01e51f4e28d1e58f31ea0e3342a3606af5af5e4f40a346846d3f77237eea3dae47660d74bb9c59590363dd5da7bdc8449686639077c3d5cc5df8f8de33e99551422f3dc33e4fcff8ccc3850142c8f9890ce945e441e50acaf3cf101e77e88620507a9f2f
Seckey    : e3f37358b35bb4b7807acf7bea046db0a30fc74c6926100e5230d2e7c562e4eb9c4dfc6bb1127ef2ad37bd1500ebb95cf1ac6b88ae4bcca8ef90b41f57d2847cd5395f248599d8b561914174f228e62c2224c6d99543c8a10a6a064e3e3d2e01d51eba5df15ec75ad1bfff885931074d86aca65dc98297e9e4797f3a0109b8f40b488d133489c0c8411c18521490854118681c3606d2246e49180c9194885226659cc0696088701b32501cb224c128060a926cd382641c12021cb36421b6844242285112021ba0251ca6042114010443448938659418295c3831c42806004768a224511034095a304d22200821a54d02366420b58c54c82543245021369018042a09b50d0ab88801018ca2828d12866519006c52c4499c2266e02601c21821e242691a198a50864dda2800a484259b428419134609302253406c42400053a8485b18655c00705c8064c4a24c913460188160ccb64c8b063292b685c4000ea3b871039610d30401d2c88008116ad38871c1186120b43188842c001451a3022891c6044b800809c130242285199990cc822422a0490b902c91a6094100254b027261804d8b226cda846814004a1a890522448550c62ca0228194048d0495504c121008946ce486256402400c114e1c48029830928a2850a0b6651903285a926052445084184a5c041258441020309219467100a685a286290386054c325213b2005ab260db1690d1b89158164919b58d4408892032880201641918728c026500214611812d03198c52400c08b30903898d4c008c14c030208940d3384e483824e3487084a62122445290c68cdb008e01070a0cc62c84168c19018019000210c248d0268c02940cc424521a092118b0252349815aa660c8066820c7001341129044266112324bb46892385109444d19110913350e0a458e8b32040ab88803a204844224d846910b012000490509498688c6810a012a93201212317224b650e0b029004006c4a62503914ce38008021471440820011741d4344923c12590922d838071208431c39471003092c3b68182408d02c90590b06889a6311b462de1208e043092c2488d614849ccc65120b241ca446a230185e1b891c2322219970d6182641a278a11164c49a66903347080b81083c0811b1850944482dc400694c4718ab801e20671c0902d1a322e5c34804ab0510421100a98611339924214329a30881c8101e336091ab604c4164908477209c741c1068d61382dcc1431a1b269d10628261021cb24df3553583f9f1dbfc8080f6f2047fc35d72b76e012d46fb54a5c4b0939bf2066d606397f6b433b2d1fef9100c23b0643dfe12083a0e9bf8f65e48ace8e0f4e4816a4a84becd9765c1faa25a3b4f825e7982d4fd4dc7bcc441a2a4712dbe73b1d4b8574924bb0c8797f9f62a902f3f5a723dace4fd7fad53b7ec70c9b89cb86402a8690b700693716cca8ebf721b4b6d7180dcd39393021d1670ef8db147f1bed828d645c816ff78531ae3a5c3635072b09380f596be3a2d35ad74aec50c470707c21b5d099e44dcce037d1a02dc2bbd939eb6d09ee9dd3adefe347e241681e5a2bc664a390077dccb7d10a14533837f1b95550dabd511ddd62b8ac2e893667cc0086a712b95bb7dffa694a73f579d1b60ed85905b80e2437fc53551de89fc0277ef4596a3fcabec38c3c7d740541873b0170ad0805f10daf845492d05bab771dd7800dde683c9dc154c2a8a499e48216f8af8a9b168ea5d2fd5efe3db4e1f89267dfa7acddd6ac7b665625e8dd6d265b6003dc02fd49cf4e30338f79efee84a0f69a060a4857226417fb188abffc223fa7f63bdb879dcd6d8871b3ffd153cf5a810403c85e0ef9893bbf4ad97bde7e07b336ffcd754efd013dcb9144be9707c8f327d677af87760fb1053c5b6610430174503348a4cc990d7946d7b6ed6bc9a699fc6b9a66fc5564dfd34731bba78eeb29aa9eaf11607b15dca330213b2f0a14bb2fab689a0ce558a2e12f496d35a723c7a5f898fa9e11f088af45054d0360ed1a95c6f9a1b4dea380da8b4193419a8ad71681794dfa16c074297c65c112f2578828c071b5bbfa7e9268926572a1db75ef72c078ad550a15b9ca61fbf2d1c883b83b3866b6f361f5d09414259bb2192db759c7beae61aefd13bd3ba49ef16151456051b67819494ee1929f1ce0f1fcbe7485fc2b95f340e128e00eb833d333553a2df8f7462125abe6d2c43de9caa99a3c6c4cc8155909babc3676bfb814939d23caaadd2eaf7b6514fd923a6c2ab414591b37820c75c16b5a0c8d5fdfa02aeac272a8ffb64389720a68bb8251251eca57df2aee87477e3c2dc1050efd5e76a04088ddcff1314c636b37d4e8a7e08393eeefffb50e19ccd811de84fb2245f54c714b7d93201d33ee967b39d71173cde7535e4455b19abaa8c5af90c919d9eaf31e5a93712a64f1c0b16e628498beb9502f1558cfec0ce0b382d3889114c1e9120790a03e2868aecc225b49105a7bd7ccb9b61ce7cc40d026e83617863c13ed5771fb8ab574b468503f4490d132b1c6d1cd7aa2de9e0bb7198bd4ee25551e272817f1917ebc3b2e39152de77dee6ac95331691a74419a2dc4d0cf9872323ab43ec50658cc40deabaffed0e4e2df22b4a1d68d606179f9c8f6c12683f8b8e35f2a670cf96cdc31d27568a5cc7b776a750e9ad77a3c5b8d5d9720954ce50272b1efb4ae071b7c5c652f41762bc56fd66fb36a365d69022c1673cc9d60b0daf2d7921de030b4b2857e94ba2d37cd15dd5a8b2b4eef724f46590eaea7db3de6d27c58218cf2615b8128200be932989cf9af1805c4608e4f573b898cca79aa3012ff5309745f7e55b176f66c2d36fd7b30ff1d1ecf5d22288ea7ba2a1e386900bb3e88b155a26ad50277a26d6fa8aea41d047c640caff54095fb6bc2a9c21986100a97b8788803d72888c49683994fa41828d5dd6385e7efa40af360808a04beea14c3dcd9c0eed865a8842d9081db8b6164d27507c30158afa563eeedc3ab18006a6b7f1508791a3f1bdcd723dc1dfa48f1ad9cbab33cc5f0da1802315fa0907fd803396c010ffb82f1d19739189c9a59608186d286f6c39eb3051dcf7c4e1147c853938c6a3a127c2a1540c2983c3de2d459d0d37680cba174696d3002bc424cef5439ae8dc968d77e4f012d026c81fb919ea7fd8bf97801b28c64fa8391761d8fb7c320472eece33af1b5909b064d08715995164faae81743d0fd6dabc9e0a79695cb8ef13e559b9a8ec7789aa98796cfb9a1a48b0ab5a02007be1a7218af8941fb3db521cde22c6f207dc40aeae6863bd14c2c4bf08a206b77a9018dbd00b94b5276d049221d1a2f565eaa4eb32ffa23823bacae138757f8e77d27b22891cb9bb459e070e2419c4f1252926e27a59c39e4850c7688096ca283f8bcccb6b23a8a64dc9ff2020e2b62bc33714ed9c9a09207f730231177129fb253cd2d63ebd1b209eab62ab1fea0db284049205e07cde7c3c08ca5cdf830bf0618f162fe34fc59f5181bcaa20457bd2401e3b97aea8576032bcd1b6cc641f1508ec5116df4493f6bf85cb1cbd5af36273c714dbe828cb1d68ac47b011a916a54a414d3
Message   : 48ae5de0058d4a160a5e01b674386711e0e97dd007fe57d5049e5eacc95dd957
Context   : b0f9f06e737d9f95
Signature : a0fb039e48b35e7d33c101cf955174a51bc5afcbad43e4cc8fc2d58c72308e30f3e5883d93b8c21dbe303adb9d9f8e54d2b6f0df6449a2bc7070f414aa2eac94a69cbc1fd40efcfbc93739cf97e487aad68ea26e43f1b0515e7b895705f01b05d1f4896574e44afd066622d246ffb57cf97742b0951041c88935d960420387f5191cc53c7209d813a160c83777f6e419e870f4a14016eaee142bd027fda7d6ae27070f8b81ce4e1f4a077a6e6d1163e97b2969ea845e206a738e58deda76d328bcad0755a219f0641738e6d27f073aeac8d7dcea94e2e161f69f8fd843965b5edcfa1be7fb74c2f9b4ae950a9a580baa18bd6799bb0cd7ad804b20144c26d707751b6a9c5d0f8bb4abebb2f05264bd79595ebdd58ed87fbdd525d6c11dc6a08f1db76bc4a7fcfc065a9959a3b3f50e00635885f54f9ca23acbc6db1db6c98e77bf6fc9a5381011d999af8770960f670096ca145d96e7e9e125702752d55e9002adf2c1fb45be717bc2565b60facf9813d9acc09accd46b06aaa69d198406ac6368bcb4741490a01b7da979def2f0f600a3ec560cce2ed8c0701c6aa11d254aaf4641569555328669296f47d5624fa14029dd713b24368ede308fb3053fcc62cd2b7471e1adefb2279df71693c0eb3ae5b18a86ae238d12519a3c3e4cd361d4f61ba8aab151cb23e275b8bd2009c1f293339db47d460e739d37da882cc640eef49810df97680076e7ffa9271074682f028d4b81e54796d9b32a49b26c6a8e06b372bbf1ac140212808844663244a5f1d8b55d35f5c96b4db17310b77995e560ff27e0b99b0766602757ac6e6a74d234a57ed479a808fa343970a76524eec22e81f6da0723d8770f37002dc0d1ecc3a6400979ab23f2eba7305514bead1efa86fd43dee2825645839801c0898bc560d08a391c37da5e7dced66679dcbe2bd444f46f5f730b98559324dbf4c9f2559fe506e01225c4d47438f57e5fa45a41aa474adce59eafc01cb85cdf6ad0d0236c217654628a02f05f29a32bb3ce929824fd494e2df8fa5c0ef3e33d8522b847408fd677c441d9d06a09ecb3d79c827486502d15ab00768557ad9d932507ea5d228032579ce3ee6a42b964704589adc948687341a24c5a0e604f8ac67495c7e2aa762eac65bfd06f1572dd22f51ab9bc774ff0de3ee1a5fbd591101250c70b629eef35e6b978a0c3431ffbc8a189cb86d2de3c16fe3b7d9f2473c7377b33e639bd089882208c831d37e10868e78223365c0249179d17ef74db2e914b32434af633ebfbbcc3ae73d4626d039e2400d04f6a1e48cfa1ce6c778f22e1caaeb1e48b80258043f9ed74a49b45cb9f7d2ffd62c02223f1fe4ec9ddef6303b5f0eaef97d15dfe01184b6fea7a1999d2d1dc28269370670ead04f64c719c941e681cd14408383c81a98f2fd3379971663801970e9c28e39a3550e1201b356a9ca8d797b51030231bccebfe241926b82b7771e60e70ee0bd5963cd7d2ae974747d4d3cc8d2e004e61d2e2b1cd6c05465710c1f3a5ff2f554b7608cbf3aac9e29e55e4f7d6281c5e2b8e43c5a5e117164f389b028ca6cfa3b407d75564d1d1ac7ec4603f35f6299587e69ca45d355ddd3c66e43f6853d44a6f4edd78cf15b815f784d1d95e8aa44120d78ceb4eb91beb208f866eb9128ae4e13ba8e88a1ec1ab1986c9de288dcf2f150e6bc6df54a117c45b14b57ae1b97210cdd24f83c2df6eb351d9998900753124971541581f166ff74115ea47dfd98aa68d0280df6f2907031c75a52259befa1dd387d162e1919e7f57a7f4b12f0fb85f4476bc9645d598f79930f98e653c0dc4fb2f1a5b249d4e455c55b47c865b054058173abe05c942f93f80eb087ef0ef286171682a1e967f2a8ae849288727f84c30d65851a2a87e0c87df9f059b2835b60d024ce7104c36d8e4447b614dc209876c04ec608374f95c5c59288ded2e35a6db1f6461f116661e2f9a363480e62979a272b4a713133846eaf5febfeb225ec1a457933217b90ff3e258362febcc88630bdbc293ada08c63bd119bc7985cc1681967e310b9cb844e55397d27d2daea76fee0eed190c124a750cdfd6722ad340d7e8af33937aac217b08f477bdc627baca4b371663db9898b6658728ab71fcc13f6d2d0cf336b548812ffeba4b08502250bdfa999a64682c4475ecfc48c73ab1b8bcd899ac34b1a6f3026343d7edcae666cd04760e373fe747585379fe3885a02b69942d28b54ceda933b41776b9665688d5937b1c59ad98abdc0118a8632f62848a52f57a2ed87f153a965904f442a525624f50de9cd53a6b0f0f6c5c41a64074e68c8d4b114c3d7e15bbfb8c6aeaa22b54b476e23415bc6a622d6ca759bee7fcfe016a2933aad416bf1fedc367ff2560e72c25e2a4656987b6a28def5ddf30ef170ff93c29f08a570fc757e36c98cce548861d4837416268d2345a45621c10f6653feb81baf746bc6cf12a4c1b35ac263139a0dcf82710c861c50b72c3234ca0553bd5c5940c8fc7e5fe737c876a2982252a0420925ca72932f8c5b6f7f0bbdda00c14545c163a380cc22db0c26446e07e5f9a55f57e71665b5bf1a30c3723bd1a4708d26065bd01ab543f53efa03b2182f35f4cf4f28a6d312980ee1fdbb565565f92e63d33f3b9a9e82bdb5b93333dfd929aebd7b081120577377fb42e5329248b0256f5c42c231a142311ca95209637c226a348605ded83c0a55f1c7b4af13872c97bab2bef84bbfa249d741849420bf750798701d7bbf912cb45826e711d5e5261143baaa7811393a5874d4482906a09785e11dd6f96b0c5b2b409de08d87e785c1dd226125fecc81e0160d8d705b56d82215ea98b982f737a11fd5376ff99dc57f2a21b53fd5852b03f2b761e1921806e792c8529ce9c66c9f873ab31e4b59eba9e5bff0dc7d336d01b061879d031b3bd03958826028a984ce6c4b8290606356a3db40e1f8a7c1ae33927d634104c168fab585e88e332716c8d37b2c4a6904dc6c75cc1fc305419bc60e2603ebb71b4dea0d4ddfafeb5e486a52da02c163a9f2434ea6ef6756dad105028fea8ca1563a3a58d57a6f7e0b8575213e734c97c8058cadd13dcb956d99c60e5e7d0b8cd9eb1da854def9367c2254d56c37daa29f28b5f790ef9bac20dda46c2e55e4e737708aab12e41624c70e1766de2e6c85334d8db0c6579eea0031da585d7e7228a6c51c870f2caaf607eec1a07c0632d71cd65102a8292cd31df53a93a8881ef11208cc6162a2f86d9084751d70997c8879b99aaa79b33c3a8bd08c5f94c22c2d5d61636798c4ccdce1e5ecf91015232450545b646e7a879aa0a9bbbdcfdaf6fa000e1427385877787ca9b1b5dbe3eef2fa0b1a3042496075797c8e90949699a2a5acb6b9d6dadcdef8fb000000000e22334c
Verified  : true

About

NIST FIPS 204 (ML-DSA) standard compliant, C++20, fully `constexpr`, header-only library

Topics

Resources

License

Stars

Watchers

Forks

Contributors