Skip to main content

saorsa_mls/
crypto.rs

1//! Cryptographic primitives for MLS using saorsa-pqc
2//!
3//! This module provides post-quantum cryptographic operations using
4//! NIST-standardized algorithms from the saorsa-pqc library.
5//!
6//! We use saorsa-pqc as the single source of truth for all cryptographic
7//! operations to ensure consistency and quantum-resistance.
8
9use crate::{MlsError, Result};
10use saorsa_pqc::api::{
11    hpke::{HpkeConfig, HpkeContext as PqcHpkeContext, HpkeRecipient, HpkeSender},
12    kdf::KdfAlgorithm,
13    MlDsa, MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature, MlDsaVariant, MlKem, MlKemCiphertext,
14    MlKemPublicKey, MlKemSecretKey, MlKemSharedSecret, MlKemVariant, SlhDsa, SlhDsaPublicKey,
15    SlhDsaSecretKey, SlhDsaSignature, SlhDsaVariant,
16};
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18use zeroize::{Zeroize, ZeroizeOnDrop};
19
20/// Registry identifier for Saorsa MLS PQC cipher suites (private-use values).
21///
22/// # Registry Ranges
23///
24/// - **0x0A01-0x0A04**: SPEC-PROD suites (includes hybrid X25519+MLKEM768)
25/// - **0x0B01-0x0B04**: SPEC-2 PQC-only suites (no hybrids, production use)
26#[allow(non_camel_case_types)]
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[repr(u16)]
29pub enum CipherSuiteId {
30    // SPEC-PROD registry (0x0A** - includes hybrid)
31    /// MLS_128_MLKEM768_AES128GCM_SHA256_MLDSA65 (SPEC-PROD)
32    #[deprecated(
33        since = "0.3.0",
34        note = "Use SPEC2_MLS_128_MLKEM768_AES128GCM_SHA256_MLDSA65 (0x0B01) for PQC-only"
35    )]
36    MLS_128_MLKEM768_AES128GCM_SHA256_MLDSA65 = 0x0A01,
37    /// MLS_128_HYBRID_X25519+MLKEM768_AES128GCM_SHA256_MLDSA65 (SPEC-PROD hybrid)
38    #[deprecated(
39        since = "0.3.0",
40        note = "Hybrid suites not allowed in SPEC-2 PQC-only mode"
41    )]
42    MLS_128_HYBRID_X25519_MLKEM768_AES128GCM_SHA256_MLDSA65 = 0x0A02,
43    /// MLS_256_MLKEM1024_AES256GCM_SHA512_MLDSA87 (SPEC-PROD)
44    #[deprecated(
45        since = "0.3.0",
46        note = "Use SPEC2_MLS_256_MLKEM1024_AES256GCM_SHA512_MLDSA87 (0x0B02) for PQC-only"
47    )]
48    MLS_256_MLKEM1024_AES256GCM_SHA512_MLDSA87 = 0x0A03,
49    /// MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65 (SPEC-PROD)
50    #[deprecated(
51        since = "0.3.0",
52        note = "Use SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65 (0x0B01) for PQC-only"
53    )]
54    MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65 = 0x0A04,
55
56    // SPEC-2 PQC-only registry (0x0B** - ChaCha20Poly1305 only, no hybrids, production)
57    /// MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65 (SPEC-2 PQC-only, default)
58    SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65 = 0x0B01,
59    /// MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87 (SPEC-2 PQC-only, high-security)
60    SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87 = 0x0B02,
61    /// MLS_192_MLKEM1024_CHACHA20POLY1305_SHA384_SLHDSA192 (SPEC-2 PQC-only, optional SLH-DSA)
62    #[allow(dead_code)] // Optional suite, may not be fully implemented yet
63    SPEC2_MLS_192_MLKEM1024_CHACHA20POLY1305_SHA384_SLHDSA192 = 0x0B03,
64}
65
66impl CipherSuiteId {
67    #[must_use]
68    pub const fn as_u16(self) -> u16 {
69        self as u16
70    }
71}
72
73/// Supported post-quantum / hybrid KEM choices.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
75pub enum MlsKem {
76    MlKem512,
77    MlKem768,
78    MlKem1024,
79    /// Hybrid X25519+MLKEM768 (SPEC-PROD only, not allowed in SPEC-2 PQC-only mode)
80    #[deprecated(
81        since = "0.3.0",
82        note = "Hybrid KEMs not allowed in SPEC-2 PQC-only mode"
83    )]
84    HybridX25519MlKem768,
85}
86
87/// Supported signature schemes.
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
89pub enum MlsSignature {
90    MlDsa44,
91    MlDsa65,
92    MlDsa87,
93    SlhDsa128,
94    SlhDsa192,
95    SlhDsa256,
96}
97
98/// Supported AEAD algorithms.
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
100pub enum MlsAead {
101    Aes128Gcm,
102    Aes256Gcm,
103    ChaCha20Poly1305,
104}
105
106/// Supported hash algorithms per ciphersuite definition.
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
108pub enum MlsHash {
109    Sha256,
110    Sha384,
111    Sha512,
112    Blake3,
113    Sha3_256,
114    Sha3_512,
115}
116
117/// Saorsa MLS cipher suite descriptor binding KEM, signature, AEAD, and hash choices.
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
119pub struct CipherSuite {
120    pub id: CipherSuiteId,
121    pub kem: MlsKem,
122    pub signature: MlsSignature,
123    pub aead: MlsAead,
124    pub hash: MlsHash,
125}
126
127impl CipherSuite {
128    pub const fn new(
129        id: CipherSuiteId,
130        kem: MlsKem,
131        signature: MlsSignature,
132        aead: MlsAead,
133        hash: MlsHash,
134    ) -> Self {
135        Self {
136            id,
137            kem,
138            signature,
139            aead,
140            hash,
141        }
142    }
143
144    #[must_use]
145    pub const fn id(&self) -> CipherSuiteId {
146        self.id
147    }
148
149    #[must_use]
150    pub const fn kem(&self) -> MlsKem {
151        self.kem
152    }
153
154    #[must_use]
155    pub const fn signature(&self) -> MlsSignature {
156        self.signature
157    }
158
159    #[must_use]
160    pub const fn aead(&self) -> MlsAead {
161        self.aead
162    }
163
164    #[must_use]
165    pub const fn hash(&self) -> MlsHash {
166        self.hash
167    }
168
169    #[must_use]
170    pub fn from_id(id: CipherSuiteId) -> Option<Self> {
171        REGISTRY.iter().copied().find(|suite| suite.id == id)
172    }
173
174    #[must_use]
175    pub fn all() -> &'static [CipherSuite] {
176        &REGISTRY
177    }
178
179    /// Get the ML-KEM variant for this cipher suite.
180    #[must_use]
181    #[allow(deprecated)] // Must handle deprecated HybridX25519MlKem768 for backward compatibility
182    pub fn ml_kem_variant(&self) -> MlKemVariant {
183        match self.kem {
184            MlsKem::MlKem512 => MlKemVariant::MlKem512,
185            MlsKem::MlKem768 | MlsKem::HybridX25519MlKem768 => MlKemVariant::MlKem768,
186            MlsKem::MlKem1024 => MlKemVariant::MlKem1024,
187        }
188    }
189
190    /// Get the ML-DSA variant for this cipher suite.
191    /// Check if this cipher suite uses SLH-DSA signatures
192    #[must_use]
193    pub fn uses_slh_dsa(&self) -> bool {
194        matches!(
195            self.signature,
196            MlsSignature::SlhDsa128 | MlsSignature::SlhDsa192 | MlsSignature::SlhDsa256
197        )
198    }
199
200    /// Get the ML-DSA variant for this suite.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`MlsError::CryptoError`] if the suite uses SLH-DSA signatures
205    /// (call [`Self::slh_dsa_variant`] instead, or gate on
206    /// [`Self::uses_slh_dsa`]).
207    pub fn ml_dsa_variant(&self) -> Result<MlDsaVariant> {
208        match self.signature {
209            MlsSignature::MlDsa44 => Ok(MlDsaVariant::MlDsa44),
210            MlsSignature::MlDsa65 => Ok(MlDsaVariant::MlDsa65),
211            MlsSignature::MlDsa87 => Ok(MlDsaVariant::MlDsa87),
212            MlsSignature::SlhDsa128 | MlsSignature::SlhDsa192 | MlsSignature::SlhDsa256 => Err(
213                MlsError::CryptoError("ml_dsa_variant() called on an SLH-DSA suite".to_string()),
214            ),
215        }
216    }
217
218    /// Get the SLH-DSA variant for this suite.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`MlsError::CryptoError`] if the suite uses ML-DSA signatures
223    /// (call [`Self::ml_dsa_variant`] instead, or gate on
224    /// [`Self::uses_slh_dsa`]).
225    pub fn slh_dsa_variant(&self) -> Result<SlhDsaVariant> {
226        match self.signature {
227            // Use "fast" variants for now as "small" variants may not be available
228            MlsSignature::SlhDsa128 => Ok(SlhDsaVariant::Sha2_128f),
229            MlsSignature::SlhDsa192 => Ok(SlhDsaVariant::Sha2_128f), // 128f for 192-bit level
230            MlsSignature::SlhDsa256 => Ok(SlhDsaVariant::Sha2_256f),
231            MlsSignature::MlDsa44 | MlsSignature::MlDsa65 | MlsSignature::MlDsa87 => Err(
232                MlsError::CryptoError("slh_dsa_variant() called on an ML-DSA suite".to_string()),
233            ),
234        }
235    }
236
237    /// Get the key size for symmetric encryption.
238    #[must_use]
239    pub fn key_size(&self) -> usize {
240        match self.aead {
241            MlsAead::Aes128Gcm => 16,
242            MlsAead::Aes256Gcm | MlsAead::ChaCha20Poly1305 => 32,
243        }
244    }
245
246    /// Get the nonce size for AEAD.
247    #[must_use]
248    pub fn nonce_size(&self) -> usize {
249        12
250    }
251
252    /// Get the hash output size used for HKDF and transcript hashing.
253    #[must_use]
254    pub fn hash_size(&self) -> usize {
255        match self.hash {
256            MlsHash::Sha256 => 32,
257            MlsHash::Sha384 => 48,
258            MlsHash::Sha512 => 64,
259            MlsHash::Blake3 => 32,
260            MlsHash::Sha3_256 => 32,
261            MlsHash::Sha3_512 => 64,
262        }
263    }
264
265    /// Check if this is a PQC-only cipher suite (SPEC-2 compliant).
266    ///
267    /// Returns `true` for SPEC-2 suites (0x0B01-0x0B03) which are PQC-only with ChaCha20Poly1305.
268    /// Returns `false` for SPEC-PROD suites (0x0A01-0x0A04) which may include hybrids.
269    ///
270    /// # SPEC-2 Policy (ยง8)
271    ///
272    /// SPEC-2 requires rejecting any non-PQC suite and uses only ChaCha20Poly1305 AEAD.
273    #[must_use]
274    #[allow(deprecated)] // Must check deprecated HybridX25519MlKem768 for backward compatibility
275    pub fn is_pqc_only(&self) -> bool {
276        // Check if using hybrid KEM (not allowed in SPEC-2)
277        if matches!(self.kem, MlsKem::HybridX25519MlKem768) {
278            return false;
279        }
280
281        // Check if suite ID is in SPEC-2 range (0x0B01-0x0B03)
282        matches!(
283            self.id,
284            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65
285                | CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87
286                | CipherSuiteId::SPEC2_MLS_192_MLKEM1024_CHACHA20POLY1305_SHA384_SLHDSA192
287        )
288    }
289
290    /// Check if this suite is in the SPEC-2 registry range (0x0B**).
291    #[must_use]
292    pub fn is_spec2(&self) -> bool {
293        (self.id.as_u16() & 0xFF00) == 0x0B00
294    }
295
296    /// Check if this suite is deprecated (SPEC-PROD range 0x0A**).
297    #[must_use]
298    pub fn is_deprecated(&self) -> bool {
299        (self.id.as_u16() & 0xFF00) == 0x0A00
300    }
301}
302
303impl Default for CipherSuite {
304    fn default() -> Self {
305        // SPEC-2 default: MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65 (0x0B01)
306        // All SPEC-2 suites use ChaCha20Poly1305 AEAD
307        CipherSuite::new(
308            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
309            MlsKem::MlKem768,
310            MlsSignature::MlDsa65,
311            MlsAead::ChaCha20Poly1305,
312            MlsHash::Sha256,
313        )
314    }
315}
316
317/// Cipher suite registry containing both SPEC-PROD (0x0A**) and SPEC-2 (0x0B**) suites.
318///
319/// SPEC-2 PQC-only suites (0x0B01-0x0B03) use ChaCha20Poly1305 and are preferred for production.
320/// SPEC-PROD suites (0x0A01-0x0A04) are deprecated and include hybrid mode.
321#[allow(deprecated)]
322const REGISTRY: [CipherSuite; 7] = [
323    // SPEC-PROD registry (0x0A** - deprecated, includes hybrid)
324    CipherSuite::new(
325        CipherSuiteId::MLS_128_MLKEM768_AES128GCM_SHA256_MLDSA65,
326        MlsKem::MlKem768,
327        MlsSignature::MlDsa65,
328        MlsAead::Aes128Gcm,
329        MlsHash::Sha256,
330    ),
331    CipherSuite::new(
332        CipherSuiteId::MLS_128_HYBRID_X25519_MLKEM768_AES128GCM_SHA256_MLDSA65,
333        MlsKem::HybridX25519MlKem768,
334        MlsSignature::MlDsa65,
335        MlsAead::Aes128Gcm,
336        MlsHash::Sha256,
337    ),
338    CipherSuite::new(
339        CipherSuiteId::MLS_256_MLKEM1024_AES256GCM_SHA512_MLDSA87,
340        MlsKem::MlKem1024,
341        MlsSignature::MlDsa87,
342        MlsAead::Aes256Gcm,
343        MlsHash::Sha512,
344    ),
345    CipherSuite::new(
346        CipherSuiteId::MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
347        MlsKem::MlKem768,
348        MlsSignature::MlDsa65,
349        MlsAead::ChaCha20Poly1305,
350        MlsHash::Sha256,
351    ),
352    // SPEC-2 PQC-only registry (0x0B** - ChaCha20Poly1305 only, no hybrids, production)
353    CipherSuite::new(
354        CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
355        MlsKem::MlKem768,
356        MlsSignature::MlDsa65,
357        MlsAead::ChaCha20Poly1305,
358        MlsHash::Sha256,
359    ),
360    CipherSuite::new(
361        CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
362        MlsKem::MlKem1024,
363        MlsSignature::MlDsa87,
364        MlsAead::ChaCha20Poly1305,
365        MlsHash::Sha512,
366    ),
367    // SPEC-2 optional SLH-DSA suite (0x0B03)
368    CipherSuite::new(
369        CipherSuiteId::SPEC2_MLS_192_MLKEM1024_CHACHA20POLY1305_SHA384_SLHDSA192,
370        MlsKem::MlKem1024,
371        MlsSignature::SlhDsa192,
372        MlsAead::ChaCha20Poly1305,
373        MlsHash::Sha384,
374    ),
375];
376
377/// Cryptographic operations using saorsa-pqc as the single source of truth
378pub struct Hash {
379    pub suite: CipherSuite,
380}
381
382impl Hash {
383    #[must_use]
384    pub fn new(suite: CipherSuite) -> Self {
385        Self { suite }
386    }
387
388    /// Compute hash of data using BLAKE3 from saorsa-pqc
389    #[must_use]
390    pub fn hash(&self, data: &[u8]) -> Vec<u8> {
391        use saorsa_pqc::api::hash::Blake3Hasher;
392        use saorsa_pqc::api::traits::Hash as HashTrait;
393
394        let mut hasher = Blake3Hasher::new();
395        hasher.update(data);
396        let output = hasher.finalize();
397        output.as_ref().to_vec()
398    }
399
400    /// Compute HMAC using saorsa-pqc's HMAC-SHA3-256
401    ///
402    /// # Errors
403    ///
404    /// Returns `MlsError::CryptoError` if the HMAC key is invalid or computation fails.
405    pub fn hmac(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
406        use saorsa_pqc::api::hmac::HmacSha3_256;
407        use saorsa_pqc::api::traits::Mac;
408
409        let mut mac = HmacSha3_256::new(key)
410            .map_err(|e| MlsError::CryptoError(format!("HMAC key error: {e:?}")))?;
411        mac.update(data);
412        let output = mac.finalize();
413        Ok(output.as_ref().to_vec())
414    }
415}
416
417/// Key derivation using HKDF
418#[derive(Debug, Clone)]
419pub struct KeySchedule {
420    suite: CipherSuite,
421}
422
423impl KeySchedule {
424    #[must_use]
425    pub fn new(suite: CipherSuite) -> Self {
426        Self { suite }
427    }
428
429    /// Derive key using saorsa-pqc's HKDF-SHA3-256
430    ///
431    /// # Errors
432    ///
433    /// Returns `MlsError::CryptoError` if the HKDF key derivation fails.
434    pub fn derive_secret(&self, secret: &[u8], label: &str, context: &[u8]) -> Result<Vec<u8>> {
435        use saorsa_pqc::api::kdf::HkdfSha3_256;
436        use saorsa_pqc::api::traits::Kdf;
437
438        let info = Self::build_hkdf_label(label, context, self.suite.hash_size());
439        let mut output = vec![0u8; self.suite.hash_size()];
440
441        HkdfSha3_256::derive(secret, None, &info, &mut output)
442            .map_err(|e| MlsError::CryptoError(format!("HKDF error: {e:?}")))?;
443        Ok(output)
444    }
445
446    /// Derive multiple keys using saorsa-pqc's HKDF
447    ///
448    /// # Errors
449    ///
450    /// Returns `MlsError::CryptoError` if any key derivation fails.
451    pub fn derive_keys(
452        &self,
453        salt: &[u8],
454        secret: &[u8],
455        labels: &[&str],
456        lengths: &[usize],
457    ) -> Result<Vec<Vec<u8>>> {
458        let mut results = Vec::new();
459
460        for (label, &length) in labels.iter().zip(lengths.iter()) {
461            let key = self.derive_secret(secret, label, salt)?;
462            results.push(key[..length].to_vec());
463        }
464        Ok(results)
465    }
466
467    /// Derive a single key with specific length using saorsa-pqc's HKDF
468    ///
469    /// # Errors
470    ///
471    /// Returns `MlsError::CryptoError` if the HKDF key derivation fails.
472    pub fn derive_key(
473        &self,
474        salt: &[u8],
475        secret: &[u8],
476        info: &[u8],
477        length: usize,
478    ) -> Result<Vec<u8>> {
479        use saorsa_pqc::api::kdf::HkdfSha3_256;
480        use saorsa_pqc::api::traits::Kdf;
481
482        let mut output = vec![0u8; length];
483        HkdfSha3_256::derive(secret, Some(salt), info, &mut output)
484            .map_err(|e| MlsError::CryptoError(format!("HKDF error: {e:?}")))?;
485        Ok(output)
486    }
487
488    fn build_hkdf_label(label: &str, context: &[u8], length: usize) -> Vec<u8> {
489        let mut info = Vec::new();
490        info.extend_from_slice(&u16::try_from(length).unwrap_or(u16::MAX).to_be_bytes());
491        info.push(
492            u8::try_from(b"tls13 ".len()).unwrap_or(u8::MAX)
493                + u8::try_from(label.len()).unwrap_or(u8::MAX),
494        );
495        info.extend_from_slice(b"tls13 ");
496        info.extend_from_slice(label.as_bytes());
497        info.push(u8::try_from(context.len()).unwrap_or(u8::MAX));
498        info.extend_from_slice(context);
499        info
500    }
501
502    /// Derive an exported secret per RFC 9420 ยง8.5
503    ///
504    /// This implements the MLS exporter interface which allows applications
505    /// to derive secrets from the group's exporter secret.
506    ///
507    /// # Errors
508    ///
509    /// Returns `MlsError::CryptoError` if the key derivation fails.
510    pub fn export_secret(
511        &self,
512        exporter_secret: &[u8],
513        label: &str,
514        context: &[u8],
515        length: usize,
516    ) -> Result<Vec<u8>> {
517        use saorsa_pqc::api::kdf::HkdfSha3_256;
518        use saorsa_pqc::api::traits::Kdf;
519
520        // Build the exporter label per RFC 9420
521        // The label is "mls exporter" || user_label
522        let full_label = format!("mls exporter {}", label);
523        let info = Self::build_hkdf_label(&full_label, context, length);
524
525        let mut output = vec![0u8; length];
526        HkdfSha3_256::derive(exporter_secret, None, &info, &mut output)
527            .map_err(|e| MlsError::CryptoError(format!("Exporter derivation failed: {e:?}")))?;
528
529        Ok(output)
530    }
531}
532
533/// Signature supporting both ML-DSA and SLH-DSA
534#[derive(Clone)]
535pub enum Signature {
536    /// ML-DSA signature
537    MlDsa(MlDsaSignature),
538    /// SLH-DSA signature
539    SlhDsa(SlhDsaSignature),
540}
541
542impl Signature {
543    /// Convert to bytes
544    pub fn to_bytes(&self) -> Vec<u8> {
545        match self {
546            Signature::MlDsa(sig) => sig.to_bytes().to_vec(),
547            Signature::SlhDsa(sig) => sig.to_bytes().to_vec(),
548        }
549    }
550}
551
552/// Debug wrapper for `Signature` to provide Debug, PartialEq, and Serialize traits
553#[derive(Clone)]
554pub struct DebugSignature(pub Signature);
555
556impl std::fmt::Debug for DebugSignature {
557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558        match &self.0 {
559            Signature::MlDsa(sig) => write!(f, "MlDsaSignature(<{} bytes>)", sig.to_bytes().len()),
560            Signature::SlhDsa(sig) => {
561                write!(f, "SlhDsaSignature(<{} bytes>)", sig.to_bytes().len())
562            }
563        }
564    }
565}
566
567impl PartialEq for DebugSignature {
568    fn eq(&self, other: &Self) -> bool {
569        self.0.to_bytes() == other.0.to_bytes()
570    }
571}
572
573impl Eq for DebugSignature {}
574
575impl serde::Serialize for DebugSignature {
576    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
577    where
578        S: serde::Serializer,
579    {
580        serializer.serialize_bytes(&self.0.to_bytes())
581    }
582}
583
584impl<'de> serde::Deserialize<'de> for DebugSignature {
585    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
586    where
587        D: serde::Deserializer<'de>,
588    {
589        let bytes = <Vec<u8>>::deserialize(deserializer)?;
590        // Note: We cannot deserialize without knowing the variant, so this will need context
591        // For now, assume ML-DSA-65 for backward compatibility
592        let signature = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &bytes)
593            .map_err(|e| serde::de::Error::custom(format!("Signature decode error: {e:?}")))?;
594        Ok(DebugSignature(Signature::MlDsa(signature)))
595    }
596}
597
598/// Signature key pair supporting both ML-DSA and SLH-DSA
599#[derive(Clone)]
600pub enum SignatureKey {
601    /// ML-DSA signature keys
602    MlDsa {
603        /// Secret key for signing
604        secret: MlDsaSecretKey,
605        /// Public key for verification
606        public: MlDsaPublicKey,
607    },
608    /// SLH-DSA signature keys
609    SlhDsa {
610        /// Secret key for signing
611        secret: SlhDsaSecretKey,
612        /// Public key for verification
613        public: SlhDsaPublicKey,
614    },
615}
616
617/// Post-quantum key pair for signing and key agreement
618pub struct KeyPair {
619    /// Signature key pair (ML-DSA or SLH-DSA)
620    pub signature_key: SignatureKey,
621    /// ML-KEM secret key for decapsulation
622    pub kem_secret: MlKemSecretKey,
623    /// ML-KEM public key for encapsulation
624    pub kem_public: MlKemPublicKey,
625    /// Cipher suite
626    pub suite: CipherSuite,
627}
628
629impl std::fmt::Debug for KeyPair {
630    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
631        f.debug_struct("KeyPair")
632            .field("suite", &self.suite)
633            .field("verifying_key", &"<hidden>")
634            .field("kem_public", &"<hidden>")
635            .finish_non_exhaustive()
636    }
637}
638
639impl KeyPair {
640    /// Generate a new key pair
641    ///
642    /// # Aborts
643    ///
644    /// Aborts the process if the OS CSPRNG-backed key generation fails โ€” an
645    /// unrecoverable security invariant, mirroring [`random_bytes`]. (This
646    /// cannot happen in practice.)
647    #[must_use]
648    pub fn generate(suite: CipherSuite) -> Self {
649        // Generate signature key pair based on suite type
650        let signature_key = if suite.uses_slh_dsa() {
651            // `uses_slh_dsa()` guarantees `slh_dsa_variant()` is Ok; abort on the
652            // impossible internal-inconsistency case (matches keygen handling).
653            let variant = suite
654                .slh_dsa_variant()
655                .unwrap_or_else(|_| std::process::abort());
656            let slh_dsa = SlhDsa::new(variant);
657            let (public, secret) = slh_dsa
658                .generate_keypair()
659                .unwrap_or_else(|_| std::process::abort());
660            SignatureKey::SlhDsa { secret, public }
661        } else {
662            let variant = suite
663                .ml_dsa_variant()
664                .unwrap_or_else(|_| std::process::abort());
665            let ml_dsa = MlDsa::new(variant);
666            let (public, secret) = ml_dsa
667                .generate_keypair()
668                .unwrap_or_else(|_| std::process::abort());
669            SignatureKey::MlDsa { secret, public }
670        };
671
672        // Generate ML-KEM key pair for key encapsulation
673        let ml_kem = MlKem::new(suite.ml_kem_variant());
674        let (kem_public, kem_secret) = ml_kem
675            .generate_keypair()
676            .unwrap_or_else(|_| std::process::abort());
677
678        Self {
679            signature_key,
680            kem_secret,
681            kem_public,
682            suite,
683        }
684    }
685
686    /// Deterministically generate a combined signature + KEM key pair from
687    /// `seed`. The same `(suite, seed)` always yields the same keypair, via
688    /// `saorsa-pqc`'s FIPS-203/204 seeded keygen
689    /// (`MlKem::generate_keypair_from_seed(d, z)`,
690    /// `MlDsa::generate_keypair_from_seed(xi)`). `seed` may be any length; it is
691    /// HKDF-expanded into independent, domain-separated sub-seeds.
692    ///
693    /// Note: only ML-DSA suites are deterministic โ€” `saorsa-pqc` provides no
694    /// seeded keygen for SLH-DSA, so SLH-DSA suites return an error.
695    ///
696    /// # Errors
697    ///
698    /// Returns [`MlsError::CryptoError`] for an SLH-DSA suite or if HKDF fails.
699    pub fn generate_from_seed(suite: CipherSuite, seed: &[u8]) -> Result<Self> {
700        use saorsa_pqc::api::kdf::HkdfSha3_256;
701        use saorsa_pqc::api::traits::Kdf;
702
703        if suite.uses_slh_dsa() {
704            return Err(MlsError::CryptoError(
705                "deterministic key generation is not supported for SLH-DSA suites".to_string(),
706            ));
707        }
708
709        // Expand the seed into independent, domain-separated sub-seeds.
710        let mut sig_xi = [0u8; 32];
711        let mut kem_d = [0u8; 32];
712        let mut kem_z = [0u8; 32];
713        HkdfSha3_256::derive(seed, None, b"saorsa-mls keypair v1 ml-dsa xi", &mut sig_xi)
714            .map_err(|e| MlsError::CryptoError(format!("HKDF error: {e:?}")))?;
715        HkdfSha3_256::derive(seed, None, b"saorsa-mls keypair v1 ml-kem d", &mut kem_d)
716            .map_err(|e| MlsError::CryptoError(format!("HKDF error: {e:?}")))?;
717        HkdfSha3_256::derive(seed, None, b"saorsa-mls keypair v1 ml-kem z", &mut kem_z)
718            .map_err(|e| MlsError::CryptoError(format!("HKDF error: {e:?}")))?;
719
720        let ml_dsa = MlDsa::new(suite.ml_dsa_variant()?);
721        let (public, secret) = ml_dsa.generate_keypair_from_seed(&sig_xi);
722        let signature_key = SignatureKey::MlDsa { secret, public };
723
724        let ml_kem = MlKem::new(suite.ml_kem_variant());
725        let (kem_public, kem_secret) = ml_kem.generate_keypair_from_seed(&kem_d, &kem_z);
726
727        sig_xi.zeroize();
728        kem_d.zeroize();
729        kem_z.zeroize();
730
731        Ok(Self {
732            signature_key,
733            kem_secret,
734            kem_public,
735            suite,
736        })
737    }
738
739    /// Get the public verification key bytes
740    #[must_use]
741    pub fn verifying_key_bytes(&self) -> Vec<u8> {
742        match &self.signature_key {
743            SignatureKey::MlDsa { public, .. } => public.to_bytes().to_vec(),
744            SignatureKey::SlhDsa { public, .. } => public.to_bytes().to_vec(),
745        }
746    }
747
748    /// Get the ML-DSA public key. Use [`Self::verifying_key_bytes`] for the
749    /// suite-agnostic form.
750    ///
751    /// # Errors
752    ///
753    /// Returns [`MlsError::CryptoError`] if this is an SLH-DSA keypair.
754    pub fn verifying_key(&self) -> Result<&MlDsaPublicKey> {
755        match &self.signature_key {
756            SignatureKey::MlDsa { public, .. } => Ok(public),
757            SignatureKey::SlhDsa { .. } => Err(MlsError::CryptoError(
758                "verifying_key() called on an SLH-DSA keypair".to_string(),
759            )),
760        }
761    }
762
763    /// Get the public KEM key
764    #[must_use]
765    pub fn public_key(&self) -> &MlKemPublicKey {
766        &self.kem_public
767    }
768
769    /// Sign a message
770    ///
771    /// # Errors
772    ///
773    /// Returns `MlsError::CryptoError` if the signing operation fails.
774    pub fn sign(&self, message: &[u8]) -> Result<Signature> {
775        match &self.signature_key {
776            SignatureKey::MlDsa { secret, .. } => {
777                let ml_dsa = MlDsa::new(self.suite.ml_dsa_variant()?);
778                ml_dsa
779                    .sign(secret, message)
780                    .map(Signature::MlDsa)
781                    .map_err(|e| MlsError::CryptoError(format!("ML-DSA signing failed: {e:?}")))
782            }
783            SignatureKey::SlhDsa { secret, .. } => {
784                let slh_dsa = SlhDsa::new(self.suite.slh_dsa_variant()?);
785                slh_dsa
786                    .sign(secret, message)
787                    .map(Signature::SlhDsa)
788                    .map_err(|e| MlsError::CryptoError(format!("SLH-DSA signing failed: {e:?}")))
789            }
790        }
791    }
792
793    /// Verify a signature
794    ///
795    /// Returns `true` if signature is valid, `false` otherwise.
796    pub fn verify(&self, message: &[u8], signature: &Signature) -> bool {
797        match (&self.signature_key, signature) {
798            (SignatureKey::MlDsa { public, .. }, Signature::MlDsa(sig)) => {
799                let Ok(variant) = self.suite.ml_dsa_variant() else {
800                    return false;
801                };
802                let ml_dsa = MlDsa::new(variant);
803                ml_dsa.verify(public, message, sig).unwrap_or(false)
804            }
805            (SignatureKey::SlhDsa { public, .. }, Signature::SlhDsa(sig)) => {
806                let Ok(variant) = self.suite.slh_dsa_variant() else {
807                    return false;
808                };
809                let slh_dsa = SlhDsa::new(variant);
810                slh_dsa.verify(public, message, sig).unwrap_or(false)
811            }
812            // Signature type mismatch
813            _ => false,
814        }
815    }
816
817    /// Perform key encapsulation
818    ///
819    /// # Errors
820    ///
821    /// Returns `MlsError::CryptoError` if the encapsulation operation fails.
822    pub fn encapsulate(
823        &self,
824        recipient_public: &MlKemPublicKey,
825    ) -> Result<(MlKemCiphertext, MlKemSharedSecret)> {
826        let ml_kem = MlKem::new(self.suite.ml_kem_variant());
827        let (shared_secret, ciphertext) = ml_kem
828            .encapsulate(recipient_public)
829            .map_err(|e| MlsError::CryptoError(format!("Encapsulation failed: {e:?}")))?;
830        Ok((ciphertext, shared_secret))
831    }
832
833    /// Perform key decapsulation (for FIPS KATs)
834    ///
835    /// # Errors
836    ///
837    /// Returns `MlsError::CryptoError` if the decapsulation operation fails.
838    pub fn kem_decaps(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
839        let ml_kem = MlKem::new(self.suite.ml_kem_variant());
840
841        // Convert bytes to ciphertext
842        let ct = MlKemCiphertext::from_bytes(self.suite.ml_kem_variant(), ciphertext)
843            .map_err(|e| MlsError::CryptoError(format!("Invalid ciphertext: {e:?}")))?;
844
845        let shared_secret = ml_kem
846            .decapsulate(&self.kem_secret, &ct)
847            .map_err(|e| MlsError::CryptoError(format!("Decapsulation failed: {e:?}")))?;
848
849        Ok(shared_secret.to_bytes().to_vec())
850    }
851
852    /// Perform key decapsulation
853    ///
854    /// # Errors
855    ///
856    /// Returns `MlsError::CryptoError` if the decapsulation operation fails.
857    pub fn decapsulate(&self, ciphertext: &MlKemCiphertext) -> Result<MlKemSharedSecret> {
858        let ml_kem = MlKem::new(self.suite.ml_kem_variant());
859        ml_kem
860            .decapsulate(&self.kem_secret, ciphertext)
861            .map_err(|e| MlsError::CryptoError(format!("Decapsulation failed: {e:?}")))
862    }
863}
864
865/// AEAD encryption/decryption using saorsa-pqc's `ChaCha20Poly1305`
866#[derive(Debug)]
867pub struct AeadCipher {
868    key: Vec<u8>,
869    suite: CipherSuite,
870}
871
872impl AeadCipher {
873    /// Create a new AEAD cipher from key material
874    ///
875    /// # Errors
876    ///
877    /// Returns `MlsError::CryptoError` if the key size is invalid.
878    pub fn new(key: Vec<u8>, suite: CipherSuite) -> Result<Self> {
879        if key.len() != suite.key_size() {
880            return Err(MlsError::CryptoError(format!(
881                "Invalid key size: expected {}, got {}",
882                suite.key_size(),
883                key.len()
884            )));
885        }
886
887        Ok(Self { key, suite })
888    }
889
890    /// Encrypt plaintext with associated data using saorsa-pqc's `ChaCha20Poly1305`
891    ///
892    /// # Errors
893    ///
894    /// Returns `MlsError::CryptoError` if the nonce size is invalid or encryption fails.
895    pub fn encrypt(
896        &self,
897        nonce: &[u8],
898        plaintext: &[u8],
899        associated_data: &[u8],
900    ) -> Result<Vec<u8>> {
901        // Use saorsa-pqc's ChaCha20Poly1305
902        use saorsa_pqc::api::symmetric::ChaCha20Poly1305 as PqcCipher;
903
904        if nonce.len() != self.suite.nonce_size() {
905            return Err(MlsError::CryptoError("Invalid nonce size".to_string()));
906        }
907
908        // Convert key to the format expected by saorsa-pqc
909        let key_array: [u8; 32] = self
910            .key
911            .clone()
912            .try_into()
913            .map_err(|_| MlsError::CryptoError("Invalid key size".to_string()))?;
914        let key = chacha20poly1305::Key::from(key_array);
915
916        let cipher = PqcCipher::new(&key);
917
918        // Convert nonce
919        let nonce_array: [u8; 12] = nonce
920            .try_into()
921            .map_err(|_| MlsError::CryptoError("Invalid nonce size".to_string()))?;
922        let nonce_obj = chacha20poly1305::Nonce::from(nonce_array);
923
924        // Encrypt with AAD
925        let ciphertext = cipher
926            .encrypt_with_aad(&nonce_obj, plaintext, associated_data)
927            .map_err(|e| MlsError::CryptoError(format!("Encryption failed: {e:?}")))?;
928
929        // Return nonce + ciphertext for wire format
930        let mut result = nonce.to_vec();
931        result.extend_from_slice(&ciphertext);
932        Ok(result)
933    }
934
935    /// Decrypt ciphertext with associated data using saorsa-pqc's `ChaCha20Poly1305`
936    ///
937    /// # Errors
938    ///
939    /// Returns `MlsError::CryptoError` if the nonce size is invalid, ciphertext is too short, or decryption fails.
940    pub fn decrypt(
941        &self,
942        nonce: &[u8],
943        ciphertext: &[u8],
944        associated_data: &[u8],
945    ) -> Result<Vec<u8>> {
946        // Use saorsa-pqc's ChaCha20Poly1305
947        use saorsa_pqc::api::symmetric::ChaCha20Poly1305 as PqcCipher;
948
949        if nonce.len() != self.suite.nonce_size() {
950            return Err(MlsError::CryptoError("Invalid nonce size".to_string()));
951        }
952
953        // The ciphertext includes the nonce at the beginning (12 bytes)
954        // Skip it and use it for decryption
955        if ciphertext.len() < 12 {
956            return Err(MlsError::CryptoError("Ciphertext too short".to_string()));
957        }
958
959        let actual_nonce = &ciphertext[..12];
960        let actual_ciphertext = &ciphertext[12..];
961
962        // Convert key to the format expected by saorsa-pqc
963        let key_array: [u8; 32] = self
964            .key
965            .clone()
966            .try_into()
967            .map_err(|_| MlsError::CryptoError("Invalid key size".to_string()))?;
968        let key = chacha20poly1305::Key::from(key_array);
969
970        let cipher = PqcCipher::new(&key);
971
972        // Convert nonce
973        let nonce_array: [u8; 12] = actual_nonce
974            .try_into()
975            .map_err(|_| MlsError::CryptoError("Invalid nonce size".to_string()))?;
976        let nonce_obj = chacha20poly1305::Nonce::from(nonce_array);
977
978        // Decrypt with AAD
979        let plaintext = cipher
980            .decrypt_with_aad(&nonce_obj, actual_ciphertext, associated_data)
981            .map_err(|e| MlsError::CryptoError(format!("Decryption failed: {e:?}")))?;
982
983        Ok(plaintext)
984    }
985
986    /// Get the key size for this cipher
987    #[must_use]
988    pub fn key_size(&self) -> usize {
989        self.suite.key_size()
990    }
991
992    /// Get the nonce size for this cipher
993    #[must_use]
994    pub fn nonce_size(&self) -> usize {
995        self.suite.nonce_size()
996    }
997}
998
999/// Generate cryptographically secure random bytes via the OS CSPRNG.
1000///
1001/// # Panics
1002///
1003/// Aborts the process if the OS random number generator is unavailable.
1004/// This is an unrecoverable security invariant โ€” cryptographic operations
1005/// cannot proceed safely without a functioning CSPRNG.
1006#[must_use]
1007pub fn random_bytes(len: usize) -> Vec<u8> {
1008    let mut bytes = vec![0u8; len];
1009    getrandom::fill(&mut bytes).unwrap_or_else(|_| std::process::abort());
1010    bytes
1011}
1012
1013/// Constant-time comparison using subtle crate (already a dependency of saorsa-pqc)
1014#[must_use]
1015pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
1016    use subtle::ConstantTimeEq;
1017    a.ct_eq(b).into()
1018}
1019
1020/// MLS-specific KDF labels
1021pub mod labels {
1022    pub const ENCRYPTION: &str = "encryption";
1023    pub const AUTHENTICATION: &str = "authentication";
1024    pub const EXPORTER: &str = "exporter";
1025    pub const EXTERNAL: &str = "external";
1026    pub const CONFIRM: &str = "confirm";
1027    pub const MEMBERSHIP: &str = "membership";
1028    pub const RESUMPTION: &str = "resumption";
1029    pub const INIT: &str = "init";
1030    pub const SENDER_DATA: &str = "sender data";
1031    pub const WELCOME: &str = "welcome";
1032    pub const HANDSHAKE: &str = "handshake";
1033    pub const APPLICATION: &str = "application";
1034    // Additional MLS labels
1035    pub const EPOCH_SECRET: &str = "epoch";
1036    pub const SENDER_DATA_SECRET: &str = "sender data secret";
1037    pub const HANDSHAKE_SECRET: &str = "handshake secret";
1038    pub const APPLICATION_SECRET: &str = "application secret";
1039    pub const EXPORTER_SECRET: &str = "exporter secret";
1040    pub const AUTHENTICATION_SECRET: &str = "authentication secret";
1041    pub const EXTERNAL_SECRET: &str = "external secret";
1042    pub const CONFIRMATION_KEY: &str = "confirmation key";
1043    pub const MEMBERSHIP_KEY: &str = "membership key";
1044    pub const RESUMPTION_PSK: &str = "resumption psk";
1045    pub const INIT_SECRET: &str = "init secret";
1046}
1047
1048/// Secure bytes that are zeroed on drop
1049#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)]
1050pub struct SecretBytes {
1051    inner: Vec<u8>,
1052}
1053
1054impl SecretBytes {
1055    #[must_use]
1056    pub fn new(bytes: Vec<u8>) -> Self {
1057        Self { inner: bytes }
1058    }
1059
1060    #[must_use]
1061    pub fn as_bytes(&self) -> &[u8] {
1062        &self.inner
1063    }
1064
1065    #[must_use]
1066    pub fn len(&self) -> usize {
1067        self.inner.len()
1068    }
1069
1070    #[must_use]
1071    pub fn is_empty(&self) -> bool {
1072        self.inner.is_empty()
1073    }
1074}
1075
1076impl From<Vec<u8>> for SecretBytes {
1077    fn from(bytes: Vec<u8>) -> Self {
1078        Self::new(bytes)
1079    }
1080}
1081
1082/// HPKE context for encryption/decryption operations
1083///
1084/// Wraps saorsa-pqc's HPKE context and provides MLS-specific interface
1085pub struct HpkeContext {
1086    inner: PqcHpkeContext,
1087}
1088
1089impl HpkeContext {
1090    /// Export secret material for key derivation
1091    ///
1092    /// # Errors
1093    ///
1094    /// Returns `MlsError::CryptoError` if the export operation fails
1095    pub fn export(&mut self, context: &[u8], length: usize) -> Result<Vec<u8>> {
1096        self.inner
1097            .export(context, length)
1098            .map_err(|e| MlsError::CryptoError(format!("HPKE export failed: {e:?}")))
1099    }
1100
1101    /// Seal (encrypt) plaintext with associated data
1102    ///
1103    /// # Errors
1104    ///
1105    /// Returns `MlsError::CryptoError` if encryption fails
1106    pub fn seal(&mut self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
1107        self.inner
1108            .seal(plaintext, aad)
1109            .map_err(|e| MlsError::CryptoError(format!("HPKE seal failed: {e:?}")))
1110    }
1111
1112    /// Open (decrypt) ciphertext with associated data
1113    ///
1114    /// # Errors
1115    ///
1116    /// Returns `MlsError::CryptoError` if decryption or authentication fails
1117    pub fn open(&mut self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
1118        self.inner
1119            .open(ciphertext, aad)
1120            .map_err(|e| MlsError::CryptoError(format!("HPKE open failed: {e:?}")))
1121    }
1122}
1123
1124impl CipherSuite {
1125    /// Get HPKE configuration for this ciphersuite
1126    fn hpke_config(&self) -> HpkeConfig {
1127        HpkeConfig {
1128            kem: self.ml_kem_variant(),
1129            kdf: match self.hash {
1130                MlsHash::Sha256 | MlsHash::Sha3_256 | MlsHash::Blake3 => KdfAlgorithm::HkdfSha3_256,
1131                MlsHash::Sha384 | MlsHash::Sha512 | MlsHash::Sha3_512 => KdfAlgorithm::HkdfSha3_512,
1132            },
1133            aead: match self.aead {
1134                MlsAead::ChaCha20Poly1305 => saorsa_pqc::api::aead::AeadCipher::ChaCha20Poly1305,
1135                // Note: saorsa-pqc only has AES256GCM, using it for both AES128 and AES256
1136                MlsAead::Aes128Gcm | MlsAead::Aes256Gcm => {
1137                    saorsa_pqc::api::aead::AeadCipher::Aes256Gcm
1138                }
1139            },
1140        }
1141    }
1142
1143    /// Single-shot HPKE seal operation
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns `MlsError::CryptoError` if HPKE setup or encryption fails
1148    pub fn hpke_seal(
1149        &self,
1150        recipient_public_key: &MlKemPublicKey,
1151        plaintext: &[u8],
1152        aad: &[u8],
1153        info: &[u8],
1154    ) -> Result<(Vec<u8>, Vec<u8>)> {
1155        let config = self.hpke_config();
1156        let sender = HpkeSender::new(config);
1157
1158        // Convert public key to bytes
1159        let pk_bytes = recipient_public_key.to_bytes();
1160
1161        // Setup and get context
1162        let (encapped_key, mut ctx) = sender
1163            .setup_base(&pk_bytes, info)
1164            .map_err(|e| MlsError::CryptoError(format!("HPKE setup failed: {e:?}")))?;
1165
1166        // Seal the plaintext
1167        let ciphertext = ctx
1168            .seal(plaintext, aad)
1169            .map_err(|e| MlsError::CryptoError(format!("HPKE seal failed: {e:?}")))?;
1170
1171        Ok((encapped_key, ciphertext))
1172    }
1173
1174    /// KEM encapsulation with seed (for FIPS KATs)
1175    ///
1176    /// Note: Current implementation uses standard encapsulation.
1177    /// Deterministic encapsulation from seed requires RNG seeding support
1178    /// in saorsa-pqc which is pending upstream.
1179    ///
1180    /// # Errors
1181    ///
1182    /// Returns `MlsError::CryptoError` if encapsulation fails
1183    pub fn kem_encaps_with_seed(
1184        &self,
1185        recipient_public_key: &MlKemPublicKey,
1186        _seed: &[u8],
1187    ) -> Result<(Vec<u8>, Vec<u8>)> {
1188        // TODO: Use seed when saorsa-pqc provides RNG seeding APIs
1189        // For now, use standard encapsulation to validate algorithm correctness
1190        let ml_kem = MlKem::new(self.ml_kem_variant());
1191        let (shared_secret, ciphertext) = ml_kem
1192            .encapsulate(recipient_public_key)
1193            .map_err(|e| MlsError::CryptoError(format!("Encapsulation failed: {e:?}")))?;
1194
1195        Ok((ciphertext.to_bytes(), shared_secret.to_bytes().to_vec()))
1196    }
1197
1198    /// Setup HPKE sender context
1199    ///
1200    /// Returns encapsulated key and sender context for multiple encryptions
1201    ///
1202    /// # Errors
1203    ///
1204    /// Returns `MlsError::CryptoError` if HPKE setup fails
1205    pub fn hpke_setup_sender(
1206        &self,
1207        recipient_public_key: &MlKemPublicKey,
1208        info: &[u8],
1209    ) -> Result<(Vec<u8>, HpkeContext)> {
1210        let config = self.hpke_config();
1211        let sender = HpkeSender::new(config);
1212
1213        let pk_bytes = recipient_public_key.to_bytes();
1214
1215        let (encapped_key, ctx) = sender
1216            .setup_base(&pk_bytes, info)
1217            .map_err(|e| MlsError::CryptoError(format!("HPKE sender setup failed: {e:?}")))?;
1218
1219        Ok((encapped_key, HpkeContext { inner: ctx }))
1220    }
1221}
1222
1223impl KeyPair {
1224    /// Single-shot HPKE open operation
1225    ///
1226    /// # Errors
1227    ///
1228    /// Returns `MlsError::CryptoError` if HPKE setup or decryption fails
1229    pub fn hpke_open(
1230        &self,
1231        encapped_key: &[u8],
1232        ciphertext: &[u8],
1233        aad: &[u8],
1234        info: &[u8],
1235    ) -> Result<Vec<u8>> {
1236        let mut ctx = self.hpke_setup_receiver(encapped_key, info)?;
1237        ctx.open(ciphertext, aad)
1238    }
1239
1240    /// Setup HPKE receiver context
1241    ///
1242    /// Returns receiver context for multiple decryptions
1243    ///
1244    /// # Errors
1245    ///
1246    /// Returns `MlsError::CryptoError` if HPKE setup fails
1247    pub fn hpke_setup_receiver(&self, encapped_key: &[u8], info: &[u8]) -> Result<HpkeContext> {
1248        let config = HpkeConfig {
1249            kem: self.suite.ml_kem_variant(),
1250            kdf: match self.suite.hash {
1251                MlsHash::Sha256 | MlsHash::Sha3_256 | MlsHash::Blake3 => KdfAlgorithm::HkdfSha3_256,
1252                MlsHash::Sha384 | MlsHash::Sha512 | MlsHash::Sha3_512 => KdfAlgorithm::HkdfSha3_512,
1253            },
1254            aead: match self.suite.aead {
1255                MlsAead::ChaCha20Poly1305 => saorsa_pqc::api::aead::AeadCipher::ChaCha20Poly1305,
1256                // Note: saorsa-pqc only has AES256GCM, using it for both AES128 and AES256
1257                MlsAead::Aes128Gcm | MlsAead::Aes256Gcm => {
1258                    saorsa_pqc::api::aead::AeadCipher::Aes256Gcm
1259                }
1260            },
1261        };
1262
1263        let recipient = HpkeRecipient::new(config);
1264
1265        // Convert secret key to bytes
1266        let sk_bytes = self.kem_secret.to_bytes();
1267
1268        let ctx = recipient
1269            .setup_base(encapped_key, &sk_bytes, info)
1270            .map_err(|e| MlsError::CryptoError(format!("HPKE recipient setup failed: {e:?}")))?;
1271
1272        Ok(HpkeContext { inner: ctx })
1273    }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279
1280    #[test]
1281    fn test_cipher_suite_defaults() {
1282        let suite = CipherSuite::default();
1283        // SPEC-2: Default is ChaCha20Poly1305 (0x0B01)
1284        assert_eq!(
1285            suite.id(),
1286            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65
1287        );
1288        assert_eq!(suite.key_size(), 32); // ChaCha20 = 32 bytes
1289        assert_eq!(suite.nonce_size(), 12);
1290        assert!(suite.is_pqc_only());
1291        assert!(suite.is_spec2());
1292    }
1293
1294    #[test]
1295    fn test_hash_operations() {
1296        let hash = Hash::new(CipherSuite::default());
1297        let data = b"test data";
1298        let result = hash.hash(data);
1299        assert_eq!(result.len(), 32);
1300
1301        let key = b"test key";
1302        let hmac_result = hash.hmac(key, data).unwrap();
1303        assert!(!hmac_result.is_empty());
1304    }
1305
1306    #[test]
1307    fn test_key_generation() {
1308        let kp1 = KeyPair::generate(CipherSuite::default());
1309        let kp2 = KeyPair::generate(CipherSuite::default());
1310
1311        // Keys should be different
1312        assert_ne!(kp1.verifying_key_bytes(), kp2.verifying_key_bytes());
1313    }
1314
1315    #[test]
1316    fn test_signing_and_verification() {
1317        let kp = KeyPair::generate(CipherSuite::default());
1318        let message = b"test message";
1319
1320        let signature = kp.sign(message).unwrap();
1321
1322        // Test with correct message
1323        assert!(kp.verify(message, &signature));
1324
1325        // Test with wrong message - should NOT verify
1326        let wrong_message = b"wrong message";
1327        assert!(
1328            !kp.verify(wrong_message, &signature),
1329            "Signature should not verify with wrong message"
1330        );
1331    }
1332
1333    #[test]
1334    fn test_key_encapsulation() {
1335        let kp1 = KeyPair::generate(CipherSuite::default());
1336        let kp2 = KeyPair::generate(CipherSuite::default());
1337
1338        // Encapsulate for kp2
1339        let (ciphertext, shared_secret1) = kp1.encapsulate(&kp2.kem_public).unwrap();
1340
1341        // Decapsulate with kp2's secret
1342        let shared_secret2 = kp2.decapsulate(&ciphertext).unwrap();
1343
1344        // Shared secrets should match
1345        assert_eq!(shared_secret1.to_bytes(), shared_secret2.to_bytes());
1346    }
1347
1348    #[test]
1349    fn test_aead_encryption() {
1350        // Use ChaCha20Poly1305 suite since AEAD implementation currently only supports that
1351        let suite = CipherSuite::from_id(
1352            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
1353        )
1354        .expect("ChaCha20 suite should exist");
1355        let key = random_bytes(suite.key_size()); // Use correct key size for suite
1356        let cipher = AeadCipher::new(key, suite).unwrap();
1357        let nonce = random_bytes(12);
1358        let plaintext = b"secret message";
1359        let aad = b"associated data";
1360
1361        let ciphertext = cipher.encrypt(&nonce, plaintext, aad).unwrap();
1362        let decrypted = cipher.decrypt(&nonce, &ciphertext, aad).unwrap();
1363
1364        assert_eq!(decrypted, plaintext);
1365    }
1366
1367    #[test]
1368    fn test_key_derivation() {
1369        let ks = KeySchedule::new(CipherSuite::default());
1370        let secret = random_bytes(32);
1371        let derived = ks.derive_secret(&secret, "test", b"context").unwrap();
1372        assert_eq!(derived.len(), 32);
1373    }
1374
1375    #[test]
1376    fn test_secret_bytes_zeroize() {
1377        let data = vec![1, 2, 3, 4, 5];
1378        let secret = SecretBytes::new(data.clone());
1379        assert_eq!(secret.as_bytes(), &data);
1380        assert_eq!(secret.len(), 5);
1381        assert!(!secret.is_empty());
1382        // SecretBytes will be zeroed when dropped
1383    }
1384
1385    // SPEC-2 PQC-only suite tests
1386
1387    #[test]
1388    fn test_spec2_default_suite() {
1389        let suite = CipherSuite::default();
1390        // SPEC-2 default is 0x0B01 (ChaCha20Poly1305)
1391        assert_eq!(
1392            suite.id(),
1393            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
1394            "Default should be SPEC-2 PQC-only suite 0x0B01"
1395        );
1396        assert_eq!(suite.kem(), MlsKem::MlKem768);
1397        assert_eq!(suite.signature(), MlsSignature::MlDsa65);
1398        assert_eq!(suite.aead(), MlsAead::ChaCha20Poly1305);
1399        assert_eq!(suite.hash(), MlsHash::Sha256);
1400        assert!(suite.is_pqc_only(), "Default suite must be PQC-only");
1401        assert!(suite.is_spec2(), "Default suite must be SPEC-2");
1402    }
1403
1404    #[test]
1405    fn test_spec2_suite_0xb01_chacha_sha256() {
1406        let suite = CipherSuite::from_id(
1407            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
1408        )
1409        .expect("Suite 0x0B01 should exist");
1410        assert_eq!(suite.key_size(), 32, "ChaCha20 key size");
1411        assert_eq!(suite.hash_size(), 32, "SHA256 hash size");
1412        assert_eq!(suite.aead(), MlsAead::ChaCha20Poly1305);
1413        assert!(suite.is_pqc_only());
1414        assert!(suite.is_spec2());
1415        assert!(!suite.is_deprecated());
1416    }
1417
1418    #[test]
1419    fn test_spec2_suite_0xb02_chacha_sha512() {
1420        let suite = CipherSuite::from_id(
1421            CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
1422        )
1423        .expect("Suite 0x0B02 should exist");
1424        assert_eq!(suite.kem(), MlsKem::MlKem1024, "High-security ML-KEM-1024");
1425        assert_eq!(
1426            suite.signature(),
1427            MlsSignature::MlDsa87,
1428            "High-security ML-DSA-87"
1429        );
1430        assert_eq!(suite.aead(), MlsAead::ChaCha20Poly1305);
1431        assert_eq!(suite.hash(), MlsHash::Sha512);
1432        assert_eq!(suite.key_size(), 32, "ChaCha20 key size");
1433        assert_eq!(suite.hash_size(), 64, "SHA512 hash size");
1434        assert!(suite.is_pqc_only());
1435    }
1436
1437    #[test]
1438    #[allow(deprecated)]
1439    fn test_hybrid_suite_not_pqc_only() {
1440        let suite = CipherSuite::from_id(
1441            CipherSuiteId::MLS_128_HYBRID_X25519_MLKEM768_AES128GCM_SHA256_MLDSA65,
1442        )
1443        .expect("Hybrid suite should exist for backwards compat");
1444        assert!(!suite.is_pqc_only(), "Hybrid suite must NOT be PQC-only");
1445        assert!(!suite.is_spec2(), "Hybrid suite is SPEC-PROD");
1446        assert!(suite.is_deprecated(), "Hybrid suite should be deprecated");
1447    }
1448
1449    #[test]
1450    #[allow(deprecated)]
1451    fn test_deprecated_suites_not_pqc_only() {
1452        let deprecated_ids = [
1453            CipherSuiteId::MLS_128_MLKEM768_AES128GCM_SHA256_MLDSA65,
1454            CipherSuiteId::MLS_256_MLKEM1024_AES256GCM_SHA512_MLDSA87,
1455            CipherSuiteId::MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
1456        ];
1457
1458        for id in &deprecated_ids {
1459            let suite =
1460                CipherSuite::from_id(*id).expect("Deprecated suite should still be in registry");
1461            assert!(suite.is_deprecated(), "Suite {:?} should be deprecated", id);
1462            assert!(!suite.is_spec2(), "Suite {:?} is not SPEC-2", id);
1463            // Note: Deprecated PQC-only suites (0x0A01, 0x0A03, 0x0A04) are actually PQC-only,
1464            // but we encourage migration to SPEC-2 range (0x0B**)
1465        }
1466    }
1467
1468    #[test]
1469    fn test_pqc_only_policy_enforcement() {
1470        // SPEC-2 suites must be PQC-only with ChaCha20Poly1305
1471        let spec2_ids = [
1472            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
1473            CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
1474        ];
1475
1476        for id in &spec2_ids {
1477            let suite = CipherSuite::from_id(*id).expect("SPEC-2 suite should exist");
1478            assert!(
1479                suite.is_pqc_only(),
1480                "SPEC-2 suite {:?} must be PQC-only",
1481                id
1482            );
1483            assert_eq!(
1484                suite.aead(),
1485                MlsAead::ChaCha20Poly1305,
1486                "SPEC-2 suite {:?} must use ChaCha20Poly1305",
1487                id
1488            );
1489        }
1490    }
1491
1492    #[test]
1493    fn test_sha384_hash_size() {
1494        // Test the new SHA384 hash variant with ChaCha20Poly1305
1495        let suite = CipherSuite::new(
1496            CipherSuiteId::SPEC2_MLS_192_MLKEM1024_CHACHA20POLY1305_SHA384_SLHDSA192,
1497            MlsKem::MlKem1024,
1498            MlsSignature::SlhDsa192,
1499            MlsAead::ChaCha20Poly1305,
1500            MlsHash::Sha384,
1501        );
1502        assert_eq!(suite.hash_size(), 48, "SHA384 produces 48-byte output");
1503    }
1504
1505    #[test]
1506    fn test_registry_contains_seven_suites() {
1507        let all_suites = CipherSuite::all();
1508        assert_eq!(
1509            all_suites.len(),
1510            7,
1511            "Registry should contain 4 SPEC-PROD + 3 SPEC-2 suites (including optional SLH-DSA)"
1512        );
1513    }
1514
1515    #[test]
1516    fn test_suite_id_to_u16() {
1517        assert_eq!(
1518            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65.as_u16(),
1519            0x0B01
1520        );
1521        assert_eq!(
1522            CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87.as_u16(),
1523            0x0B02
1524        );
1525        assert_eq!(
1526            CipherSuiteId::SPEC2_MLS_192_MLKEM1024_CHACHA20POLY1305_SHA384_SLHDSA192.as_u16(),
1527            0x0B03
1528        );
1529    }
1530
1531    #[test]
1532    fn test_all_spec2_suites_functional() {
1533        // Verify all SPEC-2 suites can perform basic crypto operations
1534        let spec2_ids = [
1535            CipherSuiteId::SPEC2_MLS_128_MLKEM768_CHACHA20POLY1305_SHA256_MLDSA65,
1536            CipherSuiteId::SPEC2_MLS_256_MLKEM1024_CHACHA20POLY1305_SHA512_MLDSA87,
1537        ];
1538
1539        for id in &spec2_ids {
1540            let suite = CipherSuite::from_id(*id).expect("Suite should exist");
1541
1542            // Test key generation
1543            let kp = KeyPair::generate(suite);
1544            assert!(!kp.verifying_key_bytes().is_empty());
1545
1546            // Test signing
1547            let message = b"test message for SPEC-2";
1548            let signature = kp.sign(message).expect("Signing should succeed");
1549            assert!(
1550                kp.verify(message, &signature),
1551                "Verification should succeed for suite {:?}",
1552                id
1553            );
1554        }
1555    }
1556}
1557
1558/// Debug wrapper for `MlDsaSignature` to work around missing Debug impl
1559#[derive(Clone)]
1560pub struct DebugMlDsaSignature(pub MlDsaSignature);
1561
1562impl std::fmt::Debug for DebugMlDsaSignature {
1563    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1564        write!(f, "MlDsaSignature(<{} bytes>)", self.0.to_bytes().len())
1565    }
1566}
1567
1568impl PartialEq for DebugMlDsaSignature {
1569    fn eq(&self, other: &Self) -> bool {
1570        self.0.to_bytes() == other.0.to_bytes()
1571    }
1572}
1573
1574impl Eq for DebugMlDsaSignature {}
1575
1576impl Serialize for DebugMlDsaSignature {
1577    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1578    where
1579        S: Serializer,
1580    {
1581        serde_wrappers::serialize_ml_dsa_signature(&self.0, serializer)
1582    }
1583}
1584
1585impl<'de> Deserialize<'de> for DebugMlDsaSignature {
1586    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1587    where
1588        D: Deserializer<'de>,
1589    {
1590        let signature = serde_wrappers::deserialize_ml_dsa_signature(deserializer)?;
1591        Ok(DebugMlDsaSignature(signature))
1592    }
1593}
1594
1595/// Debug wrapper for `MlDsaPublicKey` to work around missing Debug impl
1596#[derive(Clone)]
1597pub struct DebugMlDsaPublicKey(pub MlDsaPublicKey);
1598
1599impl std::fmt::Debug for DebugMlDsaPublicKey {
1600    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1601        write!(f, "MlDsaPublicKey(<{} bytes>)", self.0.to_bytes().len())
1602    }
1603}
1604
1605impl PartialEq for DebugMlDsaPublicKey {
1606    fn eq(&self, other: &Self) -> bool {
1607        self.0.to_bytes() == other.0.to_bytes()
1608    }
1609}
1610
1611impl Eq for DebugMlDsaPublicKey {}
1612
1613impl Serialize for DebugMlDsaPublicKey {
1614    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1615    where
1616        S: Serializer,
1617    {
1618        serde_wrappers::serialize_ml_dsa_public_key(&self.0, serializer)
1619    }
1620}
1621
1622impl<'de> Deserialize<'de> for DebugMlDsaPublicKey {
1623    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1624    where
1625        D: Deserializer<'de>,
1626    {
1627        let key = serde_wrappers::deserialize_ml_dsa_public_key(deserializer)?;
1628        Ok(DebugMlDsaPublicKey(key))
1629    }
1630}
1631
1632/// Serde wrappers for saorsa-pqc types
1633pub mod serde_wrappers {
1634    use super::{
1635        DebugMlDsaPublicKey, DebugMlDsaSignature, MlDsaPublicKey, MlDsaSignature, MlKemCiphertext,
1636    };
1637    use saorsa_pqc::{MlDsaVariant, MlKemVariant};
1638    use serde::{Deserialize, Deserializer, Serializer};
1639
1640    /// Serialize `MlKemCiphertext`
1641    ///
1642    /// # Errors
1643    ///
1644    /// Returns a serialization error if the ciphertext cannot be serialized.
1645    pub fn serialize_ml_kem_ciphertext<S>(
1646        ciphertext: &MlKemCiphertext,
1647        serializer: S,
1648    ) -> std::result::Result<S::Ok, S::Error>
1649    where
1650        S: Serializer,
1651    {
1652        let bytes = ciphertext.to_bytes();
1653        let variant = ciphertext.variant();
1654        serializer.serialize_str(&format!("{}:{}", variant as u8, hex::encode(&bytes)))
1655    }
1656
1657    /// Deserialize `MlKemCiphertext`
1658    ///
1659    /// # Errors
1660    ///
1661    /// Returns a deserialization error if the ciphertext cannot be deserialized.
1662    pub fn deserialize_ml_kem_ciphertext<'de, D>(
1663        deserializer: D,
1664    ) -> std::result::Result<MlKemCiphertext, D::Error>
1665    where
1666        D: Deserializer<'de>,
1667    {
1668        use serde::de::Error;
1669        let s = String::deserialize(deserializer)?;
1670        let parts: Vec<&str> = s.split(':').collect();
1671        if parts.len() != 2 {
1672            return Err(D::Error::custom("Invalid MlKemCiphertext format"));
1673        }
1674
1675        let variant = match parts[0] {
1676            "0" => MlKemVariant::MlKem512,
1677            "1" => MlKemVariant::MlKem768,
1678            "2" => MlKemVariant::MlKem1024,
1679            _ => return Err(D::Error::custom("Invalid MlKemVariant")),
1680        };
1681
1682        let bytes = hex::decode(parts[1])
1683            .map_err(|e| D::Error::custom(format!("Hex decode error: {e}")))?;
1684
1685        MlKemCiphertext::from_bytes(variant, &bytes)
1686            .map_err(|e| D::Error::custom(format!("MlKemCiphertext decode error: {e:?}")))
1687    }
1688
1689    /// Serialize `MlDsaSignature`
1690    ///
1691    /// # Errors
1692    ///
1693    /// Returns a serialization error if the signature cannot be serialized.
1694    pub fn serialize_ml_dsa_signature<S>(
1695        signature: &MlDsaSignature,
1696        serializer: S,
1697    ) -> std::result::Result<S::Ok, S::Error>
1698    where
1699        S: Serializer,
1700    {
1701        let bytes = signature.to_bytes();
1702        serializer.serialize_str(&hex::encode(bytes))
1703    }
1704
1705    /// Deserialize `MlDsaSignature`
1706    ///
1707    /// # Errors
1708    ///
1709    /// Returns a deserialization error if the signature cannot be deserialized.
1710    pub fn deserialize_ml_dsa_signature<'de, D>(
1711        deserializer: D,
1712    ) -> std::result::Result<MlDsaSignature, D::Error>
1713    where
1714        D: Deserializer<'de>,
1715    {
1716        use serde::de::Error;
1717        let s = String::deserialize(deserializer)?;
1718        let bytes =
1719            hex::decode(&s).map_err(|e| D::Error::custom(format!("Hex decode error: {e}")))?;
1720
1721        // Create from bytes - we'll use MlDsa65 as default
1722        if bytes.len() != 3309 {
1723            // ML-DSA-65 signature size
1724            return Err(D::Error::custom("Invalid MlDsaSignature size"));
1725        }
1726
1727        let array: [u8; 3309] = bytes
1728            .try_into()
1729            .map_err(|_| D::Error::custom("Failed to convert to array"))?;
1730
1731        MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &array)
1732            .map_err(|e| D::Error::custom(format!("MlDsaSignature decode error: {e:?}")))
1733    }
1734
1735    /// Serialize `MlDsaPublicKey`
1736    ///
1737    /// # Errors
1738    ///
1739    /// Returns a serialization error if the public key cannot be serialized.
1740    pub fn serialize_ml_dsa_public_key<S>(
1741        key: &MlDsaPublicKey,
1742        serializer: S,
1743    ) -> std::result::Result<S::Ok, S::Error>
1744    where
1745        S: Serializer,
1746    {
1747        let bytes = key.to_bytes();
1748        serializer.serialize_str(&hex::encode(bytes))
1749    }
1750
1751    /// Deserialize `MlDsaPublicKey`
1752    ///
1753    /// # Errors
1754    ///
1755    /// Returns a deserialization error if the public key cannot be deserialized.
1756    pub fn deserialize_ml_dsa_public_key<'de, D>(
1757        deserializer: D,
1758    ) -> std::result::Result<MlDsaPublicKey, D::Error>
1759    where
1760        D: Deserializer<'de>,
1761    {
1762        use serde::de::Error;
1763        let s = String::deserialize(deserializer)?;
1764        let bytes =
1765            hex::decode(&s).map_err(|e| D::Error::custom(format!("Hex decode error: {e}")))?;
1766
1767        // Create from bytes - we'll use MlDsa65 as default
1768        if bytes.len() != 1952 {
1769            // ML-DSA-65 public key size
1770            return Err(D::Error::custom("Invalid MlDsaPublicKey size"));
1771        }
1772
1773        let array: [u8; 1952] = bytes
1774            .try_into()
1775            .map_err(|_| D::Error::custom("Failed to convert to array"))?;
1776
1777        MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, &array)
1778            .map_err(|e| D::Error::custom(format!("MlDsaPublicKey decode error: {e:?}")))
1779    }
1780
1781    /// Serialize `DebugMlDsaSignature` wrapper
1782    ///
1783    /// # Errors
1784    ///
1785    /// Returns a serialization error if the signature wrapper cannot be serialized.
1786    pub fn serialize_debug_ml_dsa_signature<S>(
1787        signature: &DebugMlDsaSignature,
1788        serializer: S,
1789    ) -> std::result::Result<S::Ok, S::Error>
1790    where
1791        S: Serializer,
1792    {
1793        serialize_ml_dsa_signature(&signature.0, serializer)
1794    }
1795
1796    /// Deserialize `DebugMlDsaSignature` wrapper
1797    ///
1798    /// # Errors
1799    ///
1800    /// Returns a deserialization error if the signature wrapper cannot be deserialized.
1801    pub fn deserialize_debug_ml_dsa_signature<'de, D>(
1802        deserializer: D,
1803    ) -> std::result::Result<DebugMlDsaSignature, D::Error>
1804    where
1805        D: Deserializer<'de>,
1806    {
1807        let signature = deserialize_ml_dsa_signature(deserializer)?;
1808        Ok(DebugMlDsaSignature(signature))
1809    }
1810
1811    /// Serialize `DebugMlDsaPublicKey` wrapper
1812    ///
1813    /// # Errors
1814    ///
1815    /// Returns a serialization error if the public key wrapper cannot be serialized.
1816    pub fn serialize_debug_ml_dsa_public_key<S>(
1817        key: &DebugMlDsaPublicKey,
1818        serializer: S,
1819    ) -> std::result::Result<S::Ok, S::Error>
1820    where
1821        S: Serializer,
1822    {
1823        serialize_ml_dsa_public_key(&key.0, serializer)
1824    }
1825
1826    /// Deserialize `DebugMlDsaPublicKey` wrapper
1827    ///
1828    /// # Errors
1829    ///
1830    /// Returns a deserialization error if the public key wrapper cannot be deserialized.
1831    pub fn deserialize_debug_ml_dsa_public_key<'de, D>(
1832        deserializer: D,
1833    ) -> std::result::Result<DebugMlDsaPublicKey, D::Error>
1834    where
1835        D: Deserializer<'de>,
1836    {
1837        let key = deserialize_ml_dsa_public_key(deserializer)?;
1838        Ok(DebugMlDsaPublicKey(key))
1839    }
1840}
1841
1842// Display implementations for test assertions
1843impl std::fmt::Display for MlsAead {
1844    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1845        match self {
1846            MlsAead::Aes128Gcm => write!(f, "AES-128-GCM"),
1847            MlsAead::Aes256Gcm => write!(f, "AES-256-GCM"),
1848            MlsAead::ChaCha20Poly1305 => write!(f, "ChaCha20Poly1305"),
1849        }
1850    }
1851}
1852
1853impl std::fmt::Display for MlsHash {
1854    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1855        match self {
1856            MlsHash::Sha256 => write!(f, "SHA256"),
1857            MlsHash::Sha384 => write!(f, "SHA384"),
1858            MlsHash::Sha512 => write!(f, "SHA512"),
1859            MlsHash::Blake3 => write!(f, "BLAKE3"),
1860            MlsHash::Sha3_256 => write!(f, "SHA3-256"),
1861            MlsHash::Sha3_512 => write!(f, "SHA3-512"),
1862        }
1863    }
1864}
1865
1866impl std::fmt::Display for MlsKem {
1867    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1868        match self {
1869            MlsKem::MlKem512 => write!(f, "ML-KEM-512"),
1870            MlsKem::MlKem768 => write!(f, "ML-KEM-768"),
1871            MlsKem::MlKem1024 => write!(f, "ML-KEM-1024"),
1872            #[allow(deprecated)]
1873            MlsKem::HybridX25519MlKem768 => write!(f, "Hybrid-X25519-ML-KEM-768"),
1874        }
1875    }
1876}
1877
1878impl std::fmt::Display for MlsSignature {
1879    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1880        match self {
1881            MlsSignature::MlDsa44 => write!(f, "ML-DSA-44"),
1882            MlsSignature::MlDsa65 => write!(f, "ML-DSA-65"),
1883            MlsSignature::MlDsa87 => write!(f, "ML-DSA-87"),
1884            MlsSignature::SlhDsa128 => write!(f, "SLH-DSA-128"),
1885            MlsSignature::SlhDsa192 => write!(f, "SLH-DSA-192"),
1886            MlsSignature::SlhDsa256 => write!(f, "SLH-DSA-256"),
1887        }
1888    }
1889}