Post-quantum digital signatures for ESP32 using ML-DSA (FIPS 204), formerly known as Dilithium. Supports all three NIST security levels: ML-DSA-44, ML-DSA-65, and ML-DSA-87.
Ported from the mldsa-native reference implementation (PQCP project) with ESP32 hardware RNG support and memory optimizations for embedded use.
- FIPS 204 compliant -- All three ML-DSA security levels (44, 65, 87)
- Hardware RNG -- Uses ESP32's true random number generator (
esp_fill_random) - Memory optimized -- Reduced RAM mode for all parameter sets
- Constant-time -- Side-channel resistant operations with value barriers
- NVS key storage -- Persist ML-DSA-44 keypairs in flash across reboots
- Arduino-friendly -- Simple C++ wrapper classes (
MLDSA44,MLDSA65,MLDSA87) - NIST test vectors -- Verification against official ACVP test vectors included
ML-DSA (Module-Lattice-Based Digital Signature Algorithm) is a post-quantum digital signature scheme standardized by NIST as FIPS 204 in August 2024. It is derived from the CRYSTALS-Dilithium submission to the NIST Post-Quantum Cryptography competition. This library supports all three parameter sets: ML-DSA-44, ML-DSA-65, and ML-DSA-87.
ML-DSA is built on the hardness of the Module Learning With Errors (MLWE) and Module Short Integer Solution (MSIS) problems over polynomial rings. These problems are believed to be intractable even for large-scale quantum computers, unlike classical schemes (RSA, ECDSA) which are broken by Shor's algorithm.
The algorithm operates over the ring
The signing process uses a Fiat-Shamir with aborts approach:
- A random mask polynomial vector is sampled.
- A challenge hash is derived from the message and a commitment.
- A candidate signature is computed; if it leaks information about the secret key (checked via norm bounds), the attempt is aborted and restarted — this is what makes the scheme secure without a random oracle assumption on the signer's side.
- Quantum-resistant: security relies on lattice problems with no known efficient quantum algorithm.
- Fast: NTT-based polynomial arithmetic is efficient even on microcontrollers like ESP32.
- Compact: ML-DSA-44 has a 1,312-byte public key and 2,420-byte signature; higher levels trade size for stronger security margins.
- Deterministic keygen: keypairs can be derived from a 32-byte seed, enabling reproducible key generation and backup.
- Standardized: FIPS 204 compliance ensures interoperability and regulatory acceptance.
- Side-channel resistant: constant-time implementation using value barriers prevents timing and power analysis attacks.
| Variant | Security level | Public key | Secret key | Signature |
|---|---|---|---|---|
| ML-DSA-44 | NIST Level 2 (~AES-128) | 1,312 B | 2,560 B | 2,420 B |
| ML-DSA-65 | NIST Level 3 (~AES-192) | 1,952 B | 4,032 B | 3,309 B |
| ML-DSA-87 | NIST Level 5 (~AES-256) | 2,592 B | 4,896 B | 4,627 B |
ML-DSA-44 is the recommended choice for ESP32 due to its lower memory footprint. ML-DSA-65 and ML-DSA-87 are available when higher security margins are needed.
| Parameter | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| Public Key | 1,312 B | 1,952 B | 2,592 B |
| Secret Key | 2,560 B | 4,032 B | 4,896 B |
| Signature | 2,420 B | 3,309 B | 4,627 B |
| Seed | 32 B | 32 B | 32 B |
- ESP32 board (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, etc.)
- Arduino IDE 1.8+ or PlatformIO
- ~32KB free stack for ML-DSA-44 signing, ~45KB for ML-DSA-65, ~59KB for ML-DSA-87
- FreeRTOS task with 64KB stack (ML-DSA-44/65) or 80KB stack (ML-DSA-87)
- ~320KB free RAM minimum
- Download or clone this repository
- Copy the
mldsa-arduinofolder to your Arduino libraries directory:- Linux:
~/Arduino/libraries/ - macOS:
~/Documents/Arduino/libraries/ - Windows:
Documents\Arduino\libraries\
- Linux:
- Restart Arduino IDE
Add to your platformio.ini:
lib_deps =
https://github.com/NeuraiProject/mldsa-esp32All three variants share the same API. Just include the appropriate header and use the corresponding class:
#include <Arduino.h>
#include <MLDSA44.h> // or <MLDSA65.h> or <MLDSA87.h>
void cryptoTask(void *pvParameters) {
uint8_t pk[MLDSA44::PUBLIC_KEY_SIZE];
uint8_t sk[MLDSA44::SECRET_KEY_SIZE];
uint8_t sig[MLDSA44::SIGNATURE_SIZE];
size_t siglen;
// Generate keypair
MLDSA44::generateKeypair(pk, sk);
// Sign a message
const char *msg = "Hello, post-quantum world!";
MLDSA44::sign(sig, &siglen, (const uint8_t *)msg, strlen(msg), sk);
// Verify
int result = MLDSA44::verify(sig, siglen,
(const uint8_t *)msg, strlen(msg), pk);
// result == 0 means valid
memset(sk, 0, sizeof(sk)); // Zeroize secret key
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
// ML-DSA-44 needs ~32KB working memory, use 64KB FreeRTOS task stack
xTaskCreate(cryptoTask, "crypto", 65536, NULL, 1, NULL);
}
void loop() { delay(1000); }For ML-DSA-65 or ML-DSA-87, replace MLDSA44 with MLDSA65 or MLDSA87 respectively, and increase the FreeRTOS task stack size to 80KB for ML-DSA-87.
All three wrapper classes expose an identical API. Replace MLDSAxx with MLDSA44, MLDSA65, or MLDSA87 as needed:
#include <MLDSA44.h> // or <MLDSA65.h> or <MLDSA87.h>
// Generate a random keypair
int MLDSAxx::generateKeypair(uint8_t *pk, uint8_t *sk);
// Generate keypair from a deterministic 32-byte seed
int MLDSAxx::generateKeypairFromSeed(uint8_t *pk, uint8_t *sk,
const uint8_t *seed);
// Sign a message (optional context string)
int MLDSAxx::sign(uint8_t *sig, size_t *siglen,
const uint8_t *msg, size_t msglen,
const uint8_t *sk,
const uint8_t *ctx = nullptr, size_t ctxlen = 0);
// Verify a signature (optional context string)
int MLDSAxx::verify(const uint8_t *sig, size_t siglen,
const uint8_t *msg, size_t msglen,
const uint8_t *pk,
const uint8_t *ctx = nullptr, size_t ctxlen = 0);Each class provides size constants: PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, SIGNATURE_SIZE, SEED_SIZE.
All functions return 0 on success, negative on error.
#include <MLDSA44_NVS.h>
// Save/load keypair to/from NVS flash
bool MLDSA44_NVS::saveKeypair(const char *ns, const uint8_t *pk, const uint8_t *sk);
bool MLDSA44_NVS::loadKeypair(const char *ns, uint8_t *pk, uint8_t *sk);
bool MLDSA44_NVS::loadPublicKey(const char *ns, uint8_t *pk);
// Check existence / erase
bool MLDSA44_NVS::hasKeypair(const char *ns);
bool MLDSA44_NVS::eraseKeypair(const char *ns);
// Generate and save in one step
int MLDSA44_NVS::generateAndSave(const char *ns, uint8_t *pk, uint8_t *sk);The ns parameter is an NVS namespace string (max 15 characters).
| Example | Description |
|---|---|
| mldsa_simple | Minimal keygen + sign + verify using MLDSA44 wrapper |
| mldsa_demo_arduino | Full demo with timing, memory diagnostics, and tamper test |
| mldsa_nvs_storage | Persistent key storage in NVS flash (survives reboots) |
| mldsa_test_vectors | FIPS 204 conformance test against NIST ACVP test vectors |
With MLD_CONFIG_REDUCE_RAM and MLD_CONFIG_SERIAL_FIPS202_ONLY enabled (default in this port):
| Operation | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| KeyGen | ~33 KB | ~46 KB | ~63 KB |
| Sign | ~32 KB | ~45 KB | ~59 KB |
| Verify | ~22 KB | ~30 KB | ~40 KB |
Recommended FreeRTOS task stack: 64KB for ML-DSA-44/65, 80KB for ML-DSA-87.
- FreeRTOS task required: ML-DSA operations must run in a FreeRTOS task with sufficient stack (64KB for ML-DSA-44/65, 80KB for ML-DSA-87). The default Arduino
loop()stack (8KB) is too small. - Blocking operations: Keygen and signing take several seconds on ESP32. Run them in a dedicated task to avoid blocking other operations.
- Secret key handling: Always zeroize secret keys with
memset(sk, 0, sizeof(sk))after use. - Hardware RNG: The ESP32 TRNG provides full entropy when WiFi or Bluetooth is active. With both radios off, it falls back to a pseudo-random source seeded from hardware noise, which is still suitable for most applications.
- NVS storage: The
MLDSA44_NVShelper is currently available for ML-DSA-44 only. For ML-DSA-65/87, store keys manually using the ESP32 Preferences library.
All three ML-DSA parameter sets are supported:
| Variant | NIST Level | Classical equivalent | Recommended stack |
|---|---|---|---|
| ML-DSA-44 | Level 2 | ~AES-128 | 64 KB |
| ML-DSA-65 | Level 3 | ~AES-192 | 64 KB |
| ML-DSA-87 | Level 5 | ~AES-256 | 80 KB |
ML-DSA-44 is recommended for most ESP32 applications due to its lower memory footprint. Use ML-DSA-65 or ML-DSA-87 when higher security margins are required and sufficient RAM is available.
Licensed under the Apache License, Version 2.0.
The cryptographic implementation is derived from mldsa-native (PQCP project), which is licensed under Apache-2.0 OR ISC OR MIT. The upstream source files retain their original triple-license headers.