1use crate::error::Error as SignError;
17#[cfg(not(feature = "std"))]
18use alloc::{format, string::ToString, vec::Vec};
19use core::{fmt, marker::PhantomData};
20use dcrypt_algorithms::hash::{HashFunction, Shake256};
21use dcrypt_api::{Result as ApiResult, Signature as SignatureTrait};
22use dcrypt_params::pqc::dilithium::{
23 Dilithium2Params, Dilithium3Params, Dilithium5Params, DilithiumSchemeParams,
24};
25use rand::{CryptoRng, RngCore};
26use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
27
28#[derive(Clone, Debug, Zeroize)]
30pub struct DilithiumPublicKey(pub(crate) Vec<u8>);
31
32#[derive(Clone, Zeroize, ZeroizeOnDrop)]
38pub struct DilithiumSecretKey {
39 bytes: Vec<u8>,
40 public_key: Option<Vec<u8>>,
41}
42
43#[derive(Clone, Debug)]
45pub struct DilithiumSignatureData(pub(crate) Vec<u8>);
46
47pub type MlDsaPublicKey = DilithiumPublicKey;
49pub type MlDsaSecretKey = DilithiumSecretKey;
51pub type MlDsaSignature = DilithiumSignatureData;
53
54impl fmt::Debug for DilithiumSecretKey {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.debug_struct("DilithiumSecretKey")
57 .field("bytes", &"[REDACTED]")
58 .finish()
59 }
60}
61
62impl AsRef<[u8]> for DilithiumPublicKey {
63 fn as_ref(&self) -> &[u8] {
64 &self.0
65 }
66}
67
68impl AsMut<[u8]> for DilithiumPublicKey {
69 fn as_mut(&mut self) -> &mut [u8] {
70 &mut self.0
71 }
72}
73
74impl AsRef<[u8]> for DilithiumSecretKey {
75 fn as_ref(&self) -> &[u8] {
76 &self.bytes
77 }
78}
79
80impl AsRef<[u8]> for DilithiumSignatureData {
81 fn as_ref(&self) -> &[u8] {
82 &self.0
83 }
84}
85
86impl AsMut<[u8]> for DilithiumSignatureData {
87 fn as_mut(&mut self) -> &mut [u8] {
88 &mut self.0
89 }
90}
91
92impl DilithiumSecretKey {
93 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignError> {
102 match bytes.len() {
103 2560 => Dilithium2Params::validate_secret_key(bytes)?,
104 4032 => Dilithium3Params::validate_secret_key(bytes)?,
105 4896 => Dilithium5Params::validate_secret_key(bytes)?,
106 _ => {
107 return Err(SignError::Deserialization(format!(
108 "invalid ML-DSA expanded private key size: {} bytes",
109 bytes.len()
110 )))
111 }
112 }
113
114 Ok(Self {
115 bytes: bytes.to_vec(),
116 public_key: None,
117 })
118 }
119
120 pub fn from_bytes_with_public_key(
126 bytes: &[u8],
127 public_key: &DilithiumPublicKey,
128 ) -> Result<Self, SignError> {
129 match bytes.len() {
130 2560 => Dilithium2Params::validate_key_pair(bytes, public_key.as_ref())?,
131 4032 => Dilithium3Params::validate_key_pair(bytes, public_key.as_ref())?,
132 4896 => Dilithium5Params::validate_key_pair(bytes, public_key.as_ref())?,
133 _ => {
134 return Err(SignError::Deserialization(format!(
135 "invalid ML-DSA expanded private key size: {} bytes",
136 bytes.len()
137 )))
138 }
139 }
140
141 Ok(Self {
142 bytes: bytes.to_vec(),
143 public_key: Some(public_key.as_ref().to_vec()),
144 })
145 }
146
147 pub fn to_bytes(&self) -> &[u8] {
149 &self.bytes
150 }
151
152 pub fn public_key(&self) -> Result<DilithiumPublicKey, SignError> {
154 self.public_key
155 .as_ref()
156 .cloned()
157 .map(DilithiumPublicKey)
158 .ok_or_else(|| {
159 SignError::InvalidKey(
160 "public-key derivation is unavailable for an unpaired imported ML-DSA expanded key; import with from_bytes_with_public_key"
161 .to_string(),
162 )
163 })
164 }
165}
166
167impl DilithiumPublicKey {
168 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignError> {
170 match bytes.len() {
171 1312 => Dilithium2Params::validate_public_key(bytes)?,
172 1952 => Dilithium3Params::validate_public_key(bytes)?,
173 2592 => Dilithium5Params::validate_public_key(bytes)?,
174 _ => {
175 return Err(SignError::Deserialization(format!(
176 "invalid ML-DSA public key size: {} bytes",
177 bytes.len()
178 )))
179 }
180 }
181
182 Ok(Self(bytes.to_vec()))
183 }
184
185 pub fn to_bytes(&self) -> &[u8] {
187 &self.0
188 }
189}
190
191impl DilithiumSignatureData {
192 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignError> {
197 match bytes.len() {
198 2420 => validate_hint_encoding(bytes, 32 + 4 * 576, 80, 4)?,
199 3309 => validate_hint_encoding(bytes, 48 + 5 * 640, 55, 6)?,
200 4627 => validate_hint_encoding(bytes, 64 + 7 * 640, 75, 8)?,
201 _ => {
202 return Err(SignError::InvalidSignatureSize {
203 expected: 0,
204 actual: bytes.len(),
205 })
206 }
207 }
208
209 Ok(Self(bytes.to_vec()))
210 }
211
212 pub fn to_bytes(&self) -> &[u8] {
214 &self.0
215 }
216}
217
218fn validate_hint_encoding(
219 signature: &[u8],
220 hint_offset: usize,
221 omega: usize,
222 k: usize,
223) -> Result<(), SignError> {
224 let hint = signature
225 .get(hint_offset..)
226 .ok_or_else(|| SignError::Deserialization("truncated ML-DSA hint".to_string()))?;
227 if hint.len() != omega + k {
228 return Err(SignError::Deserialization(
229 "invalid ML-DSA hint length".to_string(),
230 ));
231 }
232
233 let (indices, boundaries) = hint.split_at(omega);
234 let mut start = 0usize;
235 for &boundary in boundaries {
236 let end = usize::from(boundary);
237 if end < start || end > omega {
238 return Err(SignError::Deserialization(
239 "non-monotonic ML-DSA hint boundaries".to_string(),
240 ));
241 }
242 if !indices[start..end].windows(2).all(|pair| pair[0] < pair[1]) {
243 return Err(SignError::Deserialization(
244 "duplicate or unsorted ML-DSA hint indices".to_string(),
245 ));
246 }
247 start = end;
248 }
249
250 if indices[start..].iter().any(|&byte| byte != 0) {
251 return Err(SignError::Deserialization(
252 "nonzero unused ML-DSA hint bytes".to_string(),
253 ));
254 }
255
256 Ok(())
257}
258
259#[doc(hidden)]
262pub trait MlDsaBackend: DilithiumSchemeParams {
263 fn validate_public_key(bytes: &[u8]) -> Result<(), SignError>;
264 fn validate_secret_key(bytes: &[u8]) -> Result<(), SignError>;
265 fn validate_key_pair(secret_key: &[u8], public_key: &[u8]) -> Result<(), SignError>;
266 fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> Result<(Vec<u8>, Vec<u8>), SignError>;
267 fn sign_with_rng<R: CryptoRng + RngCore>(
268 message: &[u8],
269 secret_key: &[u8],
270 rng: &mut R,
271 ) -> Result<Vec<u8>, SignError>;
272 fn verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> Result<(), SignError>;
273}
274
275fn fixed_array<const N: usize>(bytes: &[u8], what: &str) -> Result<[u8; N], SignError> {
276 bytes.try_into().map_err(|_| {
277 SignError::Deserialization(format!(
278 "invalid {what} size: expected {N}, got {}",
279 bytes.len()
280 ))
281 })
282}
283
284fn validate_expanded_secret_encoding(
285 bytes: &[u8],
286 eta: u16,
287 k: usize,
288 l: usize,
289) -> Result<(), SignError> {
290 let bits_per_coefficient = if eta == 2 { 3 } else { 4 };
291 let packed_secret_len = (k + l) * 256 * bits_per_coefficient / 8;
292 let packed_secret = bytes
293 .get(128..128 + packed_secret_len)
294 .ok_or_else(|| SignError::InvalidKey("truncated ML-DSA private key".to_string()))?;
295 let maximum = eta * 2;
296
297 for coefficient in 0..((k + l) * 256) {
298 let bit_offset = coefficient * bits_per_coefficient;
299 let byte_offset = bit_offset / 8;
300 let shift = bit_offset % 8;
301 let mut window = u32::from(packed_secret[byte_offset]);
302 if let Some(&next) = packed_secret.get(byte_offset + 1) {
303 window |= u32::from(next) << 8;
304 }
305 let value = (window >> shift) & ((1u32 << bits_per_coefficient) - 1);
306 if value > u32::from(maximum) {
307 return Err(SignError::InvalidKey(
308 "ML-DSA private key contains an out-of-range s1/s2 coefficient".to_string(),
309 ));
310 }
311 }
312
313 Ok(())
314}
315
316macro_rules! impl_mldsa_backend {
317 (
318 $params:ty,
319 $module:ident,
320 $verification_key:ident,
321 $signing_key:ident,
322 $signature:ident,
323 $pk_len:expr,
324 $sk_len:expr,
325 $sig_len:expr,
326 $eta:expr,
327 $k:expr,
328 $l:expr
329 ) => {
330 impl MlDsaBackend for $params {
331 fn validate_public_key(bytes: &[u8]) -> Result<(), SignError> {
332 let _ = fixed_array::<$pk_len>(bytes, "ML-DSA public key")?;
336 Ok(())
337 }
338
339 fn validate_secret_key(bytes: &[u8]) -> Result<(), SignError> {
340 let encoded = Zeroizing::new(fixed_array::<$sk_len>(
341 bytes,
342 "ML-DSA expanded private key",
343 )?);
344 validate_expanded_secret_encoding(encoded.as_ref(), $eta, $k, $l)?;
345 Ok(())
346 }
347
348 fn validate_key_pair(
349 secret_key: &[u8],
350 public_key: &[u8],
351 ) -> Result<(), SignError> {
352 Self::validate_secret_key(secret_key)?;
353 Self::validate_public_key(public_key)?;
354
355 let encoded_secret = Zeroizing::new(fixed_array::<$sk_len>(
356 secret_key,
357 "ML-DSA expanded private key",
358 )?);
359 let encoded_public = fixed_array::<$pk_len>(public_key, "ML-DSA public key")?;
360 let expected_tr = Shake256::digest(&encoded_public)
361 .map_err(|error| SignError::Hashing(error.to_string()))?;
362 if encoded_secret[64..128] != expected_tr.as_ref()[..] {
363 return Err(SignError::InvalidKey(
364 "ML-DSA private key tr does not match SHAKE256(pk, 64)".to_string(),
365 ));
366 }
367
368 let mut secret =
369 libcrux_ml_dsa::$module::$signing_key::new(*encoded_secret);
370 let validation_signature = libcrux_ml_dsa::$module::portable::sign(
371 &secret,
372 b"dcrypt ML-DSA expanded-key import validation",
373 &[],
374 [0xA5; libcrux_ml_dsa::SIGNING_RANDOMNESS_SIZE],
375 );
376 secret.as_mut_slice().zeroize();
377 let validation_signature = validation_signature.map_err(|details| {
378 SignError::InvalidKey(format!(
379 "ML-DSA expanded key cannot produce a validation signature: {details:?}"
380 ))
381 })?;
382
383 let public =
384 libcrux_ml_dsa::$module::$verification_key::new(encoded_public);
385 libcrux_ml_dsa::$module::portable::verify(
386 &public,
387 b"dcrypt ML-DSA expanded-key import validation",
388 &[],
389 &validation_signature,
390 )
391 .map_err(|details| {
392 SignError::InvalidKey(format!(
393 "ML-DSA expanded private/public key mismatch: {details:?}"
394 ))
395 })
396 }
397
398 fn keypair<R: CryptoRng + RngCore>(
399 rng: &mut R,
400 ) -> Result<(Vec<u8>, Vec<u8>), SignError> {
401 let mut seed = Zeroizing::new([
402 0u8;
403 libcrux_ml_dsa::KEY_GENERATION_RANDOMNESS_SIZE
404 ]);
405 rng.try_fill_bytes(seed.as_mut()).map_err(|details| {
406 SignError::KeyGeneration {
407 algorithm: <$params>::NAME,
408 details: details.to_string(),
409 }
410 })?;
411
412 let mut keypair =
413 libcrux_ml_dsa::$module::portable::generate_key_pair(*seed);
414 let public = keypair.verification_key.as_slice().to_vec();
415 let secret = keypair.signing_key.as_slice().to_vec();
416 keypair.signing_key.as_mut_slice().zeroize();
417 Ok((public, secret))
418 }
419
420 fn sign_with_rng<R: CryptoRng + RngCore>(
421 message: &[u8],
422 secret_key: &[u8],
423 rng: &mut R,
424 ) -> Result<Vec<u8>, SignError> {
425 let encoded = Zeroizing::new(fixed_array::<$sk_len>(
426 secret_key,
427 "ML-DSA expanded private key",
428 )?);
429 let mut randomness =
430 Zeroizing::new([0u8; libcrux_ml_dsa::SIGNING_RANDOMNESS_SIZE]);
431 rng.try_fill_bytes(randomness.as_mut()).map_err(|details| {
432 SignError::SignatureGeneration {
433 algorithm: <$params>::NAME,
434 details: details.to_string(),
435 }
436 })?;
437
438 let mut secret =
439 libcrux_ml_dsa::$module::$signing_key::new(*encoded);
440 let signature = libcrux_ml_dsa::$module::portable::sign(
441 &secret,
442 message,
443 &[],
444 *randomness,
445 );
446 secret.as_mut_slice().zeroize();
447 let signature = signature.map_err(|details| {
448 SignError::SignatureGeneration {
449 algorithm: <$params>::NAME,
450 details: format!("{details:?}"),
451 }
452 })?;
453 Ok(signature.as_slice().to_vec())
454 }
455
456 fn verify(
457 message: &[u8],
458 signature: &[u8],
459 public_key: &[u8],
460 ) -> Result<(), SignError> {
461 let encoded_key = fixed_array::<$pk_len>(public_key, "ML-DSA public key")?;
462 let encoded_signature = fixed_array::<$sig_len>(signature, "ML-DSA signature")?;
463 let public =
464 libcrux_ml_dsa::$module::$verification_key::new(encoded_key);
465 let signature =
466 libcrux_ml_dsa::$module::$signature::new(encoded_signature);
467
468 libcrux_ml_dsa::$module::portable::verify(
469 &public,
470 message,
471 &[],
472 &signature,
473 )
474 .map_err(|details| SignError::Verification {
475 algorithm: <$params>::NAME,
476 details: format!("ML-DSA signature verification failed: {details:?}"),
477 })
478 }
479 }
480 };
481}
482
483impl_mldsa_backend!(
484 Dilithium2Params,
485 ml_dsa_44,
486 MLDSA44VerificationKey,
487 MLDSA44SigningKey,
488 MLDSA44Signature,
489 1312,
490 2560,
491 2420,
492 2,
493 4,
494 4
495);
496impl_mldsa_backend!(
497 Dilithium3Params,
498 ml_dsa_65,
499 MLDSA65VerificationKey,
500 MLDSA65SigningKey,
501 MLDSA65Signature,
502 1952,
503 4032,
504 3309,
505 4,
506 6,
507 5
508);
509impl_mldsa_backend!(
510 Dilithium5Params,
511 ml_dsa_87,
512 MLDSA87VerificationKey,
513 MLDSA87SigningKey,
514 MLDSA87Signature,
515 2592,
516 4896,
517 4627,
518 2,
519 8,
520 7
521);
522
523pub struct Dilithium<P: DilithiumSchemeParams + 'static> {
525 _params: PhantomData<P>,
526}
527
528impl<P> Dilithium<P>
529where
530 P: MlDsaBackend + Send + Sync + 'static,
531{
532 pub fn sign_with_rng<R: CryptoRng + RngCore>(
537 message: &[u8],
538 secret_key: &DilithiumSecretKey,
539 rng: &mut R,
540 ) -> ApiResult<DilithiumSignatureData> {
541 let signature =
542 P::sign_with_rng(message, secret_key.as_ref(), rng).map_err(dcrypt_api::Error::from)?;
543 Ok(DilithiumSignatureData(signature))
544 }
545}
546
547impl<P> SignatureTrait for Dilithium<P>
548where
549 P: MlDsaBackend + Send + Sync + 'static,
550{
551 type PublicKey = DilithiumPublicKey;
552 type SecretKey = DilithiumSecretKey;
553 type SignatureData = DilithiumSignatureData;
554 type KeyPair = (Self::PublicKey, Self::SecretKey);
555
556 fn name() -> &'static str {
557 P::NAME
558 }
559
560 fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
561 let (public, secret) = P::keypair(rng).map_err(dcrypt_api::Error::from)?;
562 Ok((
563 DilithiumPublicKey(public.clone()),
564 DilithiumSecretKey {
565 bytes: secret,
566 public_key: Some(public),
567 },
568 ))
569 }
570
571 fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
572 keypair.0.clone()
573 }
574
575 fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
576 keypair.1.clone()
577 }
578
579 fn sign(message: &[u8], secret_key: &Self::SecretKey) -> ApiResult<Self::SignatureData> {
580 Self::sign_with_rng(message, secret_key, &mut rand::rngs::OsRng)
581 }
582
583 fn verify(
584 message: &[u8],
585 signature: &Self::SignatureData,
586 public_key: &Self::PublicKey,
587 ) -> ApiResult<()> {
588 validate_hint_encoding_for_len(signature.as_ref()).map_err(dcrypt_api::Error::from)?;
589 P::verify(message, signature.as_ref(), public_key.as_ref()).map_err(dcrypt_api::Error::from)
590 }
591}
592
593fn validate_hint_encoding_for_len(signature: &[u8]) -> Result<(), SignError> {
594 match signature.len() {
595 2420 => validate_hint_encoding(signature, 32 + 4 * 576, 80, 4),
596 3309 => validate_hint_encoding(signature, 48 + 5 * 640, 55, 6),
597 4627 => validate_hint_encoding(signature, 64 + 7 * 640, 75, 8),
598 actual => Err(SignError::InvalidSignatureSize {
599 expected: 0,
600 actual,
601 }),
602 }
603}
604
605pub type MlDsa44 = Dilithium<Dilithium2Params>;
607pub type MlDsa65 = Dilithium<Dilithium3Params>;
609pub type MlDsa87 = Dilithium<Dilithium5Params>;
611
612pub type Dilithium2 = MlDsa44;
614pub type Dilithium3 = MlDsa65;
616pub type Dilithium5 = MlDsa87;
618
619#[cfg(test)]
620mod tests;