use super::*;
use dcrypt_algorithms::hash::{HashFunction, Sha256, Shake256};
use dcrypt_internal::{ChaCha20Rng, CryptoRng, Error as RngError, RngCore};
const MESSAGE: &[u8] = b"FIPS 204 ML-DSA interoperability test";
#[derive(Clone)]
struct ReplayRng {
bytes: Vec<u8>,
position: usize,
}
impl ReplayRng {
fn from_hex(value: &str) -> Self {
Self {
bytes: hex::decode(value).unwrap(),
position: 0,
}
}
fn take(&mut self, output: &mut [u8]) {
let end = self.position + output.len();
output.copy_from_slice(
self.bytes
.get(self.position..end)
.expect("NIST replay RNG exhausted"),
);
self.position = end;
}
}
impl RngCore for ReplayRng {
fn next_u32(&mut self) -> u32 {
let mut bytes = [0u8; 4];
self.take(&mut bytes);
u32::from_le_bytes(bytes)
}
fn next_u64(&mut self) -> u64 {
let mut bytes = [0u8; 8];
self.take(&mut bytes);
u64::from_le_bytes(bytes)
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.take(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), RngError> {
self.take(dest);
Ok(())
}
}
impl CryptoRng for ReplayRng {}
fn sha256_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes).unwrap().as_ref())
}
macro_rules! roundtrip_test {
($name:ident, $scheme:ty, $pk_len:expr, $sk_len:expr, $sig_len:expr, $expected_name:expr) => {
#[test]
fn $name() {
let mut rng = ChaCha20Rng::from_seed([0x42; 32]);
let (public_key, secret_key) = <$scheme>::keypair(&mut rng).unwrap();
let signature = <$scheme>::sign_with_rng(MESSAGE, &secret_key, &mut rng).unwrap();
assert_eq!(<$scheme>::name(), $expected_name);
assert_eq!(public_key.as_ref().len(), $pk_len);
assert_eq!(secret_key.as_ref().len(), $sk_len);
assert_eq!(signature.as_ref().len(), $sig_len);
assert!(<$scheme>::verify(MESSAGE, &signature, &public_key).is_ok());
assert!(<$scheme>::verify(b"wrong message", &signature, &public_key).is_err());
let decoded_public = MlDsaPublicKey::from_bytes(public_key.as_ref()).unwrap();
let decoded_secret =
MlDsaSecretKey::from_bytes_with_public_key(secret_key.as_ref(), &public_key)
.unwrap();
let decoded_signature = MlDsaSignature::from_bytes(signature.as_ref()).unwrap();
assert_eq!(decoded_public.as_ref(), public_key.as_ref());
assert_eq!(decoded_secret.as_ref(), secret_key.as_ref());
assert_eq!(decoded_signature.as_ref(), signature.as_ref());
assert_eq!(
decoded_secret.public_key().unwrap().as_ref(),
public_key.as_ref()
);
}
};
}
#[test]
fn expanded_key_import_derives_and_validates_public_key() {
let mut rng = ChaCha20Rng::from_seed([0x35; 32]);
let (public, secret) = MlDsa44::keypair(&mut rng).unwrap();
let imported = MlDsaSecretKey::from_bytes(secret.as_ref()).unwrap();
assert_eq!(imported.public_key().unwrap().as_ref(), public.as_ref());
}
#[test]
fn paired_import_rejects_incoherent_t0_without_panicking() {
let mut rng = ChaCha20Rng::from_seed([0x36; 32]);
let (public, secret) = MlDsa44::keypair(&mut rng).unwrap();
let mut malformed = secret.as_ref().to_vec();
malformed[128 + 8 * 96..].fill(0);
assert!(MlDsaSecretKey::from_bytes_with_public_key(&malformed, &public).is_err());
}
roundtrip_test!(ml_dsa_44_roundtrip, MlDsa44, 1312, 2560, 2420, "ML-DSA-44");
roundtrip_test!(ml_dsa_65_roundtrip, MlDsa65, 1952, 4032, 3309, "ML-DSA-65");
roundtrip_test!(ml_dsa_87_roundtrip, MlDsa87, 2592, 4896, 4627, "ML-DSA-87");
#[test]
fn signing_interfaces_bind_context_randomizer_and_supplied_mu() {
let mut key_rng = ChaCha20Rng::from_seed([0x51; 32]);
let (public, secret) = MlDsa44::keypair(&mut key_rng).unwrap();
let context = b"application domain";
let deterministic =
MlDsa44::sign_deterministic_with_context(MESSAGE, context, &secret).unwrap();
let deterministic_again =
MlDsa44::sign_deterministic_with_context(MESSAGE, context, &secret).unwrap();
assert_eq!(deterministic.as_ref(), deterministic_again.as_ref());
assert!(MlDsa44::verify_with_context(MESSAGE, context, &deterministic, &public).is_ok());
assert!(
MlDsa44::verify_with_context(MESSAGE, b"wrong domain", &deterministic, &public).is_err()
);
let mut signing_rng = ChaCha20Rng::from_seed([0x52; 32]);
let randomized =
MlDsa44::sign_with_context_rng(MESSAGE, context, &secret, &mut signing_rng).unwrap();
assert_ne!(randomized.as_ref(), deterministic.as_ref());
assert!(MlDsa44::verify_with_context(MESSAGE, context, &randomized, &public).is_ok());
let mut formatted = Vec::with_capacity(2 + context.len() + MESSAGE.len());
formatted.extend_from_slice(&[0, context.len() as u8]);
formatted.extend_from_slice(context);
formatted.extend_from_slice(MESSAGE);
let internal =
MlDsa44::sign_internal_with_randomizer(&formatted, &secret, &[0x5a; 32]).unwrap();
assert!(MlDsa44::verify_internal_message(&formatted, &internal, &public).is_ok());
assert!(MlDsa44::verify_with_context(MESSAGE, context, &internal, &public).is_ok());
let mu = [0x33; 64];
let mu_signature = MlDsa44::sign_mu_with_randomizer(&mu, &secret, &[0x6b; 32]).unwrap();
assert!(MlDsa44::verify_mu(&mu, &mu_signature, &public).is_ok());
let mut wrong_mu = mu;
wrong_mu[0] ^= 1;
assert!(MlDsa44::verify_mu(&wrong_mu, &mu_signature, &public).is_err());
let oversized_context = [0u8; 256];
assert!(
MlDsa44::sign_deterministic_with_context(MESSAGE, &oversized_context, &secret).is_err()
);
assert!(
MlDsa44::verify_with_context(MESSAGE, &oversized_context, &deterministic, &public,)
.is_err()
);
}
#[test]
fn expanded_key_decoder_rejects_malformed_or_incoherent_components() {
let mut rng = ChaCha20Rng::from_seed([0x62; 32]);
let (public, secret) = MlDsa44::keypair(&mut rng).unwrap();
let mut malformed_tr = secret.as_ref().to_vec();
malformed_tr[64] ^= 1;
assert!(MlDsaSecretKey::from_bytes(&malformed_tr).is_err());
let mut noncanonical_s1 = secret.as_ref().to_vec();
noncanonical_s1[128] = (noncanonical_s1[128] & !0x07) | 0x07;
assert!(MlDsaSecretKey::from_bytes(&noncanonical_s1).is_err());
let mut incoherent_s1 = secret.as_ref().to_vec();
incoherent_s1[128] ^= 1;
assert!(MlDsaSecretKey::from_bytes(&incoherent_s1).is_err());
let mut incoherent_t0 = secret.as_ref().to_vec();
incoherent_t0[128 + 8 * 96] ^= 1;
assert!(MlDsaSecretKey::from_bytes(&incoherent_t0).is_err());
let (different_public, _) = MlDsa44::keypair(&mut rng).unwrap();
assert!(
MlDsaSecretKey::from_bytes_with_public_key(secret.as_ref(), &different_public).is_err()
);
assert!(MlDsaSecretKey::from_bytes_with_public_key(secret.as_ref(), &public).is_ok());
}
macro_rules! nist_keygen_kat {
(
$name:ident,
$scheme:ty,
$seed:expr,
$public_sha256:expr,
$secret_sha256:expr
) => {
#[test]
fn $name() {
let mut rng = ReplayRng::from_hex($seed);
let (public, secret) = <$scheme>::keypair(&mut rng).unwrap();
assert_eq!(sha256_hex(public.as_ref()), $public_sha256);
assert_eq!(sha256_hex(secret.as_ref()), $secret_sha256);
}
};
}
nist_keygen_kat!(
nist_acvp_keygen_ml_dsa_44_tc1,
MlDsa44,
"93EF2E6EF1FB08999D142ABE0295482370D3F43BDB254A78E2B0D5168ECA065F",
"6995b20ecd5cde41719035028a712ccf35b1adf53b913030423d9d6fa188d673",
"16a35d4b59f932aeada987dc689b075add0df57b4815bb103be7443ee3c1c561"
);
nist_keygen_kat!(
nist_acvp_keygen_ml_dsa_65_tc26,
MlDsa65,
"70CEFB9AED5B68E018B079DA8284B9D5CAD5499ED9C265FF73588005D85C225C",
"646b26b8d09dbc9e865b6a006c693a3127b065e62fab5fbe8b159c416462feb6",
"3894dc56a4553781d68ff0d1b6fcf1b4876085ea602fb6f8738def50ed7d4c75"
);
nist_keygen_kat!(
nist_acvp_keygen_ml_dsa_87_tc51,
MlDsa87,
"38359FBCD79582CFFE609E137EE2EFE8A8DBCBAD18BA92BB433AB4F09B49299D",
"ea374a09356e5f89be784f28f4ef938e8976cb5c4db00fbacb257663491748d4",
"a0cc3d4f703057c09b9261336ba45563d2c781d173f7fc634910698e95eee375"
);
macro_rules! tr_test {
($name:ident, $scheme:ty) => {
#[test]
fn $name() {
let mut rng = ChaCha20Rng::from_seed([0xA5; 32]);
let (public, secret) = <$scheme>::keypair(&mut rng).unwrap();
let expected_tr = Shake256::digest(public.as_ref()).unwrap();
assert_eq!(&secret.as_ref()[64..128], expected_tr.as_ref());
assert!(secret.as_ref()[96..128].iter().any(|&byte| byte != 0));
}
};
}
tr_test!(expanded_key_has_fips_tr_44, MlDsa44);
tr_test!(expanded_key_has_fips_tr_65, MlDsa65);
tr_test!(expanded_key_has_fips_tr_87, MlDsa87);
#[test]
fn signature_parser_rejects_noncanonical_hint_encodings() {
const HINT_OFFSET: usize = 32 + 4 * 576;
const OMEGA: usize = 80;
let canonical_empty_hint = vec![0u8; 2420];
assert!(MlDsaSignature::from_bytes(&canonical_empty_hint).is_ok());
let mut nonzero_padding = canonical_empty_hint.clone();
nonzero_padding[HINT_OFFSET] = 1;
assert!(MlDsaSignature::from_bytes(&nonzero_padding).is_err());
let mut duplicate_indices = canonical_empty_hint.clone();
duplicate_indices[HINT_OFFSET] = 7;
duplicate_indices[HINT_OFFSET + 1] = 7;
duplicate_indices[HINT_OFFSET + OMEGA..HINT_OFFSET + OMEGA + 4].copy_from_slice(&[2, 2, 2, 2]);
assert!(MlDsaSignature::from_bytes(&duplicate_indices).is_err());
let mut unsorted_indices = canonical_empty_hint.clone();
unsorted_indices[HINT_OFFSET] = 8;
unsorted_indices[HINT_OFFSET + 1] = 3;
unsorted_indices[HINT_OFFSET + OMEGA..HINT_OFFSET + OMEGA + 4].copy_from_slice(&[2, 2, 2, 2]);
assert!(MlDsaSignature::from_bytes(&unsorted_indices).is_err());
let mut nonmonotonic_boundaries = canonical_empty_hint;
nonmonotonic_boundaries[HINT_OFFSET] = 3;
nonmonotonic_boundaries[HINT_OFFSET + 1] = 9;
nonmonotonic_boundaries[HINT_OFFSET + OMEGA..HINT_OFFSET + OMEGA + 4]
.copy_from_slice(&[2, 1, 1, 1]);
assert!(MlDsaSignature::from_bytes(&nonmonotonic_boundaries).is_err());
}
#[test]
fn verify_rechecks_signature_canonicality_after_mutation() {
let mut rng = ChaCha20Rng::from_seed([0x19; 32]);
let (public, secret) = MlDsa44::keypair(&mut rng).unwrap();
let mut signature = MlDsa44::sign_with_rng(MESSAGE, &secret, &mut rng).unwrap();
let hint_offset = 32 + 4 * 576;
let final_boundary = hint_offset + 80 + 3;
signature.as_mut()[final_boundary] = 81;
assert!(MlDsa44::verify(MESSAGE, &signature, &public).is_err());
}
#[test]
fn verification_rejects_a_canonical_encoding_with_out_of_range_z() {
let mut rng = ChaCha20Rng::from_seed([0x1a; 32]);
let (public, secret) = MlDsa44::keypair(&mut rng).unwrap();
let signature = MlDsa44::sign_deterministic(MESSAGE, &secret).unwrap();
let mut encoded = signature.as_ref().to_vec();
encoded[32] = 0;
encoded[33] = 0;
encoded[34] &= !0x03;
let malformed = MlDsaSignature::from_bytes(&encoded).unwrap();
assert!(MlDsa44::verify(MESSAGE, &malformed, &public).is_err());
}
#[test]
fn old_32_byte_tr_plus_padding_private_key_fails_paired_import() {
let mut rng = ChaCha20Rng::from_seed([0xC7; 32]);
let (public, secret) = MlDsa44::keypair(&mut rng).unwrap();
let bytes = secret.as_ref();
let mut legacy_layout = Vec::with_capacity(bytes.len());
legacy_layout.extend_from_slice(&bytes[..96]);
legacy_layout.extend_from_slice(&bytes[128..]);
legacy_layout.extend_from_slice(&[0u8; 32]);
assert_eq!(legacy_layout.len(), 2560);
assert!(MlDsaSecretKey::from_bytes_with_public_key(&legacy_layout, &public).is_err());
}