Solidity library for verifying post-quantum digital signatures on the EVM.
It implements ML-DSA-65 signature verification, per FIPS 204, along with two non-standard variants that keep the lattice arithmetic identical and only swap the extendable output function (XOF), trading interoperability for gas cost.
| Library | XOF |
|---|---|
MLDSA65 |
SHAKE128/256, i.e. FIPS 204 as specified |
TurboMLDSA65 |
TurboSHAKE128/256, i.e. Keccak-f[1600] reduced to 12 rounds, as specified in RFC 9861 |
Keccak256XofCtrMLDSA65 |
KECCAK256 EVM opcode, run in counter mode |
Warning
This is experimental and unaudited. It accompanies one of my blog posts https://itzmeanjan.in/pages/verifying-ml-dsa-signatures-on-evm.html.
- You will need
docker, a C++20 compiler (gccorclang),python3andmake. - You have to fetch third-party dependencies, which are managed as git submodules.
git clone --recurse-submodules git@github.com:itzmeanjan/veripq.git
# or inside cloned directory
git submodule update --init --recursive libEnsure functional correctness and conformance of cryptographic components such as hashing and ML-DSA signature verfication using known answer test (KAT) vectors.
make testTest vectors are generated from NIST ACVP. Regenerate them all with following command.
make gen-katA gas probe meters the cost of verifying a signature with a 2793-byte message with a 183-byte context, for all variants of ML-DSA.
| Variant | verify gas |
|---|---|
| ML-DSA-65 | 15,432,417 |
| TurboML-DSA-65 | 12,366,017 |
| Keccak256XofCtr-ML-DSA-65 | 8,792,483 |
To pinpoint the cost of only hashing:
make profile-hashing| Variant | Total | Hashing | Share |
|---|---|---|---|
| ML-DSA-65 | 15,386,915 | 8,674,229 | 56.4% |
| TurboML-DSA-65 | 12,320,452 | 5,602,105 | 45.5% |
| Keccak256XofCtr-ML-DSA-65 | 8,802,894 | 2,118,015 | 24.1% |
Note
The lattice arithmetic is a constant ~6.7M gas across all three variants.
Install it as a Foundry dependency:
forge install itzmeanjan/veripqThen verify a signature.
import {MLDSA65} from "veripq/mldsa/MLDSA65.sol";
// pk is 1952 bytes, sig is 3309 bytes
bool ok = MLDSA65.verify(pk, message, sig);
// or, domain separate with a context string of < 256 bytes
bool okInCtx = MLDSA65.verify(pk, message, sig, ctx);TurboMLDSA65 and Keccak256XofCtrMLDSA65 expose exactly the same interface. See examples/MLDSA65Example.sol which demonstrates guarding a value update behind "Keccak256XofCtr variant of ML-DSA-65" signature verification.