use ml_dsa::signature::{Keypair, Signer, Verifier};
use ml_dsa::{
B32, EncodedVerifyingKey, MlDsa44, MlDsa65, MlDsa87, MlDsaParams, Signature, SigningKey,
VerifyingKey,
};
use zeroize::Zeroize;
pub const SEED_LEN: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MlDsaAlgorithm {
MlDsa44,
MlDsa65,
MlDsa87,
}
impl MlDsaAlgorithm {
#[must_use]
pub const fn token(self) -> &'static str {
match self {
Self::MlDsa44 => "ml-dsa-44",
Self::MlDsa65 => "ml-dsa-65",
Self::MlDsa87 => "ml-dsa-87",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum MlDsaError {
#[error("invalid ML-DSA seed length: expected {expected} bytes, got {actual}")]
BadSeedLength {
expected: usize,
actual: usize,
},
#[error("invalid ML-DSA public key length")]
BadPublicKeyLength,
#[error("invalid ML-DSA signature encoding")]
BadSignatureEncoding,
#[error("ML-DSA signing failed")]
SignFailed,
}
pub fn public_from_seed(algorithm: MlDsaAlgorithm, seed: &[u8]) -> Result<Vec<u8>, MlDsaError> {
match algorithm {
MlDsaAlgorithm::MlDsa44 => public_for::<MlDsa44>(seed),
MlDsaAlgorithm::MlDsa65 => public_for::<MlDsa65>(seed),
MlDsaAlgorithm::MlDsa87 => public_for::<MlDsa87>(seed),
}
}
pub fn sign(algorithm: MlDsaAlgorithm, seed: &[u8], message: &[u8]) -> Result<Vec<u8>, MlDsaError> {
match algorithm {
MlDsaAlgorithm::MlDsa44 => sign_for::<MlDsa44>(seed, message),
MlDsaAlgorithm::MlDsa65 => sign_for::<MlDsa65>(seed, message),
MlDsaAlgorithm::MlDsa87 => sign_for::<MlDsa87>(seed, message),
}
}
pub fn verify(
algorithm: MlDsaAlgorithm,
public_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<bool, MlDsaError> {
match algorithm {
MlDsaAlgorithm::MlDsa44 => verify_for::<MlDsa44>(public_key, message, signature),
MlDsaAlgorithm::MlDsa65 => verify_for::<MlDsa65>(public_key, message, signature),
MlDsaAlgorithm::MlDsa87 => verify_for::<MlDsa87>(public_key, message, signature),
}
}
fn public_for<P: MlDsaParams>(seed: &[u8]) -> Result<Vec<u8>, MlDsaError> {
let key = signing_key_from_seed::<P>(seed)?;
Ok(encoded_bytes(&key.verifying_key().encode()))
}
fn sign_for<P: MlDsaParams>(seed: &[u8], message: &[u8]) -> Result<Vec<u8>, MlDsaError> {
let key = signing_key_from_seed::<P>(seed)?;
let signature = key.try_sign(message).map_err(|_| MlDsaError::SignFailed)?;
Ok(encoded_bytes(&signature.encode()))
}
fn verify_for<P: MlDsaParams>(
public_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<bool, MlDsaError> {
let encoded_vk = EncodedVerifyingKey::<P>::try_from(public_key)
.map_err(|_| MlDsaError::BadPublicKeyLength)?;
let verifying_key = VerifyingKey::<P>::decode(&encoded_vk);
let Ok(signature) = Signature::<P>::try_from(signature) else {
return Err(MlDsaError::BadSignatureEncoding);
};
Ok(verifying_key.verify(message, &signature).is_ok())
}
fn signing_key_from_seed<P: MlDsaParams>(seed: &[u8]) -> Result<SigningKey<P>, MlDsaError> {
let mut seed_array = B32::try_from(seed).map_err(|_| MlDsaError::BadSeedLength {
expected: SEED_LEN,
actual: seed.len(),
})?;
let key = SigningKey::<P>::from_seed(&seed_array);
seed_array.zeroize();
Ok(key)
}
fn encoded_bytes(value: &impl AsRef<[u8]>) -> Vec<u8> {
value.as_ref().to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
const SEED: [u8; SEED_LEN] = [0x24; SEED_LEN];
const ALL: [MlDsaAlgorithm; 3] = [
MlDsaAlgorithm::MlDsa44,
MlDsaAlgorithm::MlDsa65,
MlDsaAlgorithm::MlDsa87,
];
#[test]
fn all_levels_sign_verify_round_trip() {
for algorithm in ALL {
let public = public_from_seed(algorithm, &SEED).expect("public");
let signature = sign(algorithm, &SEED, b"basil-pqc-message").expect("sign");
assert!(
verify(algorithm, &public, b"basil-pqc-message", &signature).expect("verify"),
"{} round trip",
algorithm.token()
);
}
}
#[test]
fn signing_is_deterministic_per_seed_and_message() {
for algorithm in ALL {
let first = sign(algorithm, &SEED, b"determinism").expect("sign");
let second = sign(algorithm, &SEED, b"determinism").expect("sign");
assert_eq!(first, second, "{} deterministic", algorithm.token());
}
}
#[test]
fn public_key_is_stable_per_seed() {
for algorithm in ALL {
let first = public_from_seed(algorithm, &SEED).expect("public");
let second = public_from_seed(algorithm, &SEED).expect("public");
assert_eq!(first, second, "{} stable public", algorithm.token());
}
}
#[test]
fn wrong_message_fails_verification() {
for algorithm in ALL {
let public = public_from_seed(algorithm, &SEED).expect("public");
let signature = sign(algorithm, &SEED, b"right").expect("sign");
assert!(
!verify(algorithm, &public, b"wrong", &signature).expect("verify"),
"{} wrong message",
algorithm.token()
);
}
}
#[test]
fn wrong_key_fails_verification() {
for algorithm in ALL {
let public = public_from_seed(algorithm, &[0x55; SEED_LEN]).expect("public");
let signature = sign(algorithm, &SEED, b"payload").expect("sign");
assert!(
!verify(algorithm, &public, b"payload", &signature).expect("verify"),
"{} wrong key",
algorithm.token()
);
}
}
#[test]
fn bad_seed_length_fails_closed() {
assert!(matches!(
sign(MlDsaAlgorithm::MlDsa65, &[0u8; SEED_LEN - 1], b"m"),
Err(MlDsaError::BadSeedLength { .. })
));
assert!(matches!(
public_from_seed(MlDsaAlgorithm::MlDsa44, &[0u8; SEED_LEN + 1]),
Err(MlDsaError::BadSeedLength { .. })
));
}
#[test]
fn malformed_public_key_and_signature_fail_closed() {
let public = public_from_seed(MlDsaAlgorithm::MlDsa87, &SEED).expect("public");
let signature = sign(MlDsaAlgorithm::MlDsa87, &SEED, b"m").expect("sign");
assert_eq!(
verify(MlDsaAlgorithm::MlDsa87, b"short", b"m", &signature),
Err(MlDsaError::BadPublicKeyLength)
);
assert_eq!(
verify(MlDsaAlgorithm::MlDsa87, &public, b"m", b"short"),
Err(MlDsaError::BadSignatureEncoding)
);
}
#[test]
fn known_answer_vectors_are_stable() {
use sha2::{Digest, Sha256};
struct Vector {
algorithm: MlDsaAlgorithm,
public_len: usize,
public_sha256: &'static str,
signature_len: usize,
signature_sha256: &'static str,
}
const MESSAGE: &[u8] = b"basil-pqc-kat";
let vectors = [
Vector {
algorithm: MlDsaAlgorithm::MlDsa44,
public_len: 1312,
public_sha256: "8f281f3e8a0e9cc4e3c2534be15485f1f4f343966218ec232ff8e0a69999734c",
signature_len: 2420,
signature_sha256: "bbae011ee58b80500a3daf6181654c27b3be9721192d9f2b68126160e2e4a880",
},
Vector {
algorithm: MlDsaAlgorithm::MlDsa65,
public_len: 1952,
public_sha256: "a840920f30f6acb0cbf68e91fc532da5bbc805521a7704cbef71201a5aba7ff8",
signature_len: 3309,
signature_sha256: "ef2830800d7ff9bfa64b9a1c8cd6b6b377de91f652c857614109f8dd9d25526f",
},
Vector {
algorithm: MlDsaAlgorithm::MlDsa87,
public_len: 2592,
public_sha256: "ae56b8d0ffd4bf6d09a2fdd32af657aba242265d96aeee358d5d3100587fcdc5",
signature_len: 4627,
signature_sha256: "c43ffc5347284883a1556575ba0ac1b608b4f0a0172d34deb5b76250cbf1ddcc",
},
];
let hex = |bytes: &[u8]| {
use core::fmt::Write as _;
bytes.iter().fold(String::new(), |mut acc, byte| {
let _ = write!(acc, "{byte:02x}");
acc
})
};
for vector in vectors {
let token = vector.algorithm.token();
let public = public_from_seed(vector.algorithm, &SEED).expect("public");
let signature = sign(vector.algorithm, &SEED, MESSAGE).expect("sign");
assert_eq!(public.len(), vector.public_len, "{token} public length");
assert_eq!(
signature.len(),
vector.signature_len,
"{token} signature length"
);
assert_eq!(
hex(&Sha256::digest(&public)),
vector.public_sha256,
"{token} public digest"
);
assert_eq!(
hex(&Sha256::digest(&signature)),
vector.signature_sha256,
"{token} signature digest"
);
assert!(
verify(vector.algorithm, &public, MESSAGE, &signature).expect("verify"),
"{token} known-answer verifies"
);
assert!(
!verify(vector.algorithm, &public, b"tampered", &signature).expect("verify"),
"{token} tampered message rejected"
);
}
}
#[test]
fn level_signatures_are_not_cross_verifiable() {
let public = public_from_seed(MlDsaAlgorithm::MlDsa44, &SEED).expect("public");
let signature = sign(MlDsaAlgorithm::MlDsa44, &SEED, b"m").expect("sign");
assert!(matches!(
verify(MlDsaAlgorithm::MlDsa87, &public, b"m", &signature),
Err(MlDsaError::BadPublicKeyLength | MlDsaError::BadSignatureEncoding)
));
}
}