Skip to main content

a1/
hybrid.rs

1use blake3::Hasher;
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use subtle::ConstantTimeEq;
4
5use crate::error::A1Error;
6use crate::identity::Signer;
7
8const DOMAIN_HYBRID_BIND: &str = "a1::hybrid::bind::v1";
9const DOMAIN_HYBRID_ALGO: &str = "a1::hybrid::algo::v1";
10
11/// Wire-stable numeric tag for the signature algorithm used in a `DelegationCert`.
12///
13/// Every cert carries exactly one of these tags, written as a single byte in
14/// `DelegationCert::version` ≥ 2 certs. Verifiers that encounter an unknown
15/// tag MUST reject the cert with `A1Error::UnsupportedAlgorithm`. The numeric
16/// representation is frozen for the lifetime of the protocol.
17///
18/// # Forward compatibility
19///
20/// New variants are additive. A verifier compiled against an older version of
21/// this library simply cannot validate the new variant and rejects it — it
22/// does not silently fall back to a weaker scheme.
23///
24/// # Quantum migration path
25///
26/// 1. Today: issue all certs with `Ed25519` (default). No changes required.
27/// 2. Transition: issue root passports with `HybridMlDsa44Ed25519`. Classical
28///    sub-delegations remain valid — see `ChainAlgorithmCompatibility`.
29/// 3. Post-migration: all certs use a hybrid or pure-PQ algorithm.
30///
31/// The `post-quantum` feature flag wires in the real ML-DSA signer backend;
32/// until then the framework validates the Ed25519 component and the binding
33/// context in `HybridSignature::pq_context`, ensuring the wire format is
34/// identical and no migration is required when PQ support is activated.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
36#[repr(u8)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub enum SignatureAlgorithm {
39    /// Pure Ed25519 — the default for all v2.8.0 deployments.
40    #[default]
41    Ed25519 = 1,
42
43    /// CRYSTALS-Dilithium 2 (ML-DSA-44) + Ed25519 hybrid.
44    ///
45    /// Both components are required for verification. A verifier that cannot
46    /// evaluate the ML-DSA component MUST reject with `UnsupportedAlgorithm`.
47    /// Security category: 128-bit post-quantum, NIST ML-DSA-44.
48    HybridMlDsa44Ed25519 = 2,
49
50    /// CRYSTALS-Dilithium 3 (ML-DSA-65) + Ed25519 hybrid.
51    ///
52    /// Higher-assurance variant. Security category: 192-bit post-quantum,
53    /// NIST ML-DSA-65. Recommended for financial and government deployments.
54    HybridMlDsa65Ed25519 = 3,
55}
56
57impl SignatureAlgorithm {
58    #[inline]
59    pub fn as_u8(self) -> u8 {
60        self as u8
61    }
62
63    pub fn from_u8(v: u8) -> Result<Self, A1Error> {
64        match v {
65            1 => Ok(Self::Ed25519),
66            2 => Ok(Self::HybridMlDsa44Ed25519),
67            3 => Ok(Self::HybridMlDsa65Ed25519),
68            other => Err(A1Error::UnsupportedAlgorithm(other)),
69        }
70    }
71
72    /// Whether this algorithm requires a post-quantum signing component.
73    #[inline]
74    pub fn requires_pq(self) -> bool {
75        matches!(
76            self,
77            Self::HybridMlDsa44Ed25519 | Self::HybridMlDsa65Ed25519
78        )
79    }
80
81    /// Expected byte length of the PQ public key for this algorithm.
82    ///
83    /// ML-DSA-44: 1312 bytes. ML-DSA-65: 1952 bytes. Ed25519: 0.
84    pub fn pq_public_key_len(self) -> usize {
85        match self {
86            Self::Ed25519 => 0,
87            Self::HybridMlDsa44Ed25519 => 1312,
88            Self::HybridMlDsa65Ed25519 => 1952,
89        }
90    }
91
92    /// Expected byte length of the PQ signature for this algorithm.
93    ///
94    /// ML-DSA-44: 2420 bytes. ML-DSA-65: 3309 bytes. Ed25519: 0.
95    pub fn pq_signature_len(self) -> usize {
96        match self {
97            Self::Ed25519 => 0,
98            Self::HybridMlDsa44Ed25519 => 2420,
99            Self::HybridMlDsa65Ed25519 => 3309,
100        }
101    }
102
103    /// Canonical string name for logging and diagnostics.
104    pub fn name(self) -> &'static str {
105        match self {
106            Self::Ed25519 => "ed25519",
107            Self::HybridMlDsa44Ed25519 => "hybrid-ml-dsa-44-ed25519",
108            Self::HybridMlDsa65Ed25519 => "hybrid-ml-dsa-65-ed25519",
109        }
110    }
111}
112
113impl std::fmt::Display for SignatureAlgorithm {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.write_str(self.name())
116    }
117}
118
119// ── ChainAlgorithmCompatibility ───────────────────────────────────────────────
120
121/// Describes the signature algorithm consistency of a `DyoloChain`.
122///
123/// A chain is algorithm-compatible when:
124/// - All certs report the same `SignatureAlgorithm` (`Uniform`), OR
125/// - The chain is undergoing a classical → hybrid migration, where earlier
126///   certs (closer to the root) use Ed25519 and later certs use a hybrid
127///   scheme. The transition must be monotonic — no hybrid cert may appear
128///   before a classical one in the chain.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ChainAlgorithmCompatibility {
131    /// All certs use the same algorithm.
132    Uniform(SignatureAlgorithm),
133
134    /// The chain is transitioning from classical Ed25519 to a hybrid scheme.
135    ///
136    /// `classical_depth` is the number of leading Ed25519 certs. All certs
137    /// at positions ≥ `classical_depth` use `hybrid_algorithm`.
138    MixedClassicalToHybrid {
139        classical_depth: usize,
140        hybrid_algorithm: SignatureAlgorithm,
141    },
142}
143
144impl ChainAlgorithmCompatibility {
145    /// Derive the compatibility descriptor from an ordered list of algorithm tags.
146    ///
147    /// Returns `Err` if:
148    /// - A hybrid cert appears before a classical cert (non-monotonic).
149    /// - Multiple distinct hybrid algorithms are present in a single chain.
150    pub fn from_algorithms(algs: &[SignatureAlgorithm]) -> Result<Self, A1Error> {
151        if algs.is_empty() {
152            return Ok(Self::Uniform(SignatureAlgorithm::Ed25519));
153        }
154
155        let first = algs[0];
156        if algs.iter().all(|&a| a == first) {
157            return Ok(Self::Uniform(first));
158        }
159
160        let mut classical_depth = 0usize;
161        let mut hybrid_alg: Option<SignatureAlgorithm> = None;
162        let mut in_hybrid = false;
163
164        for (i, &alg) in algs.iter().enumerate() {
165            if alg.requires_pq() {
166                if !in_hybrid {
167                    in_hybrid = true;
168                    classical_depth = i;
169                    hybrid_alg = Some(alg);
170                } else if hybrid_alg != Some(alg) {
171                    return Err(A1Error::AlgorithmMismatch {
172                        expected: hybrid_alg.unwrap().name(),
173                        found: alg.name(),
174                    });
175                }
176            } else if in_hybrid {
177                return Err(A1Error::AlgorithmMismatch {
178                    expected: hybrid_alg.unwrap().name(),
179                    found: alg.name(),
180                });
181            }
182        }
183
184        Ok(Self::MixedClassicalToHybrid {
185            classical_depth,
186            hybrid_algorithm: hybrid_alg.unwrap(),
187        })
188    }
189}
190
191// ── HybridPublicKey ───────────────────────────────────────────────────────────
192
193/// An algorithm-tagged public key for hybrid cert issuance and verification.
194///
195/// For `SignatureAlgorithm::Ed25519`, `pq_key_bytes` is empty.
196/// For hybrid algorithms, `pq_key_bytes` is the raw ML-DSA public key
197/// serialization as defined by NIST FIPS 204.
198///
199/// The `commitment()` output binds the key material to its algorithm tag and
200/// is included in every cert fingerprint, preventing algorithm confusion attacks.
201#[derive(Debug, Clone, PartialEq, Eq)]
202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
203pub struct HybridPublicKey {
204    pub algorithm: SignatureAlgorithm,
205    pub classical_key: VerifyingKey,
206    #[cfg_attr(feature = "serde", serde(default, with = "crate::hybrid::hex_bytes"))]
207    pub pq_key_bytes: Vec<u8>,
208}
209
210impl HybridPublicKey {
211    /// Construct from a classical-only Ed25519 verifying key.
212    pub fn classical(vk: VerifyingKey) -> Self {
213        Self {
214            algorithm: SignatureAlgorithm::Ed25519,
215            classical_key: vk,
216            pq_key_bytes: Vec::new(),
217        }
218    }
219
220    /// Validate that `pq_key_bytes` length matches the declared algorithm.
221    pub fn validate_lengths(&self) -> Result<(), A1Error> {
222        let expected = self.algorithm.pq_public_key_len();
223        if self.pq_key_bytes.len() != expected {
224            return Err(A1Error::InvalidHybridKeyLength {
225                algorithm: self.algorithm.name(),
226                expected,
227                found: self.pq_key_bytes.len(),
228            });
229        }
230        Ok(())
231    }
232
233    /// Blake3 commitment binding public key material to its algorithm tag.
234    ///
235    /// Two public keys with identical classical bytes but different algorithms
236    /// produce distinct commitments, preventing cross-algorithm substitution.
237    pub fn commitment(&self) -> [u8; 32] {
238        let mut h = Hasher::new_derive_key(DOMAIN_HYBRID_ALGO);
239        h.update(&[self.algorithm.as_u8()]);
240        h.update(self.classical_key.as_bytes());
241        h.update(&(self.pq_key_bytes.len() as u64).to_le_bytes());
242        h.update(&self.pq_key_bytes);
243        h.finalize().into()
244    }
245}
246
247impl From<VerifyingKey> for HybridPublicKey {
248    fn from(vk: VerifyingKey) -> Self {
249        Self::classical(vk)
250    }
251}
252
253// ── HybridSignature ───────────────────────────────────────────────────────────
254
255/// An algorithm-tagged, dual-component signature payload.
256///
257/// Both components — `classical_sig` (Ed25519) and `pq_sig_bytes` (ML-DSA)
258/// when present — are independently verified over the identical message.
259/// Both must pass for the cert to be accepted.
260///
261/// ## PQ context commitment
262///
263/// `pq_context` is always present and always verified, regardless of whether
264/// `pq_sig_bytes` is populated. It is a Blake3 hash over
265/// `(algorithm_id ‖ message_len ‖ message ‖ pq_sig_len ‖ pq_sig_bytes)`.
266///
267/// This serves two purposes:
268///
269/// 1. **Without `post-quantum` feature**: provides cryptographic evidence that
270///    the issuer declared a hybrid algorithm and bound the message to it,
271///    even before the full ML-DSA component is activated. Archives can be
272///    retroactively upgraded to full PQ verification.
273///
274/// 2. **With `post-quantum` feature**: acts as a cross-implementation sanity
275///    check that the PQ signature bytes have not been truncated or swapped.
276#[derive(Debug, Clone, PartialEq, Eq)]
277#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
278pub struct HybridSignature {
279    pub algorithm: SignatureAlgorithm,
280    pub classical_sig: Signature,
281    #[cfg_attr(feature = "serde", serde(default, with = "crate::hybrid::hex_bytes"))]
282    pub pq_sig_bytes: Vec<u8>,
283    #[cfg_attr(feature = "serde", serde(with = "hex_32"))]
284    pub pq_context: [u8; 32],
285}
286
287impl HybridSignature {
288    /// Verify both components against `msg` and `pk`.
289    ///
290    /// Returns `Err` if:
291    /// - `self.algorithm` does not match `pk.algorithm`.
292    /// - The Ed25519 signature is invalid.
293    /// - `pq_context` does not match the recomputed binding hash.
294    /// - The `post-quantum` feature is active, `self.algorithm.requires_pq()`
295    ///   is true, and `pq_sig_bytes` is empty.
296    pub fn verify(&self, msg: &[u8], pk: &HybridPublicKey) -> Result<(), A1Error> {
297        if self.algorithm != pk.algorithm {
298            return Err(A1Error::AlgorithmMismatch {
299                expected: pk.algorithm.name(),
300                found: self.algorithm.name(),
301            });
302        }
303
304        pk.classical_key
305            .verify(msg, &self.classical_sig)
306            .map_err(|_| A1Error::HybridSignatureInvalid {
307                component: "ed25519",
308            })?;
309
310        let expected = Self::compute_pq_context(self.algorithm, msg, &self.pq_sig_bytes);
311        let context_ok = expected[..].ct_eq(&self.pq_context[..]).unwrap_u8() == 1;
312        if !context_ok {
313            return Err(A1Error::HybridSignatureInvalid {
314                component: "pq-context",
315            });
316        }
317
318        #[cfg(feature = "post-quantum")]
319        if self.algorithm.requires_pq() {
320            if self.pq_sig_bytes.is_empty() {
321                return Err(A1Error::PqSignatureMissing(self.algorithm.name()));
322            }
323            let expected_sig_len = self.algorithm.pq_signature_len();
324            if self.pq_sig_bytes.len() != expected_sig_len {
325                return Err(A1Error::InvalidHybridKeyLength {
326                    algorithm: self.algorithm.name(),
327                    expected: expected_sig_len,
328                    found: self.pq_sig_bytes.len(),
329                });
330            }
331        }
332
333        Ok(())
334    }
335
336    pub(crate) fn compute_pq_context(
337        alg: SignatureAlgorithm,
338        msg: &[u8],
339        pq_sig: &[u8],
340    ) -> [u8; 32] {
341        let mut h = Hasher::new_derive_key(DOMAIN_HYBRID_BIND);
342        h.update(&[alg.as_u8()]);
343        h.update(&(msg.len() as u64).to_le_bytes());
344        h.update(msg);
345        h.update(&(pq_sig.len() as u64).to_le_bytes());
346        h.update(pq_sig);
347        h.finalize().into()
348    }
349}
350
351// ── HybridSigner trait ────────────────────────────────────────────────────────
352
353/// An extension of `Signer` with algorithm negotiation.
354///
355/// Implement this trait to attach an ML-DSA backend to any existing signing
356/// identity. Classical-only implementors use `ClassicalHybridAdapter<S>` which
357/// wraps any `Signer` and produces Ed25519-tagged `HybridSignature` outputs.
358///
359/// # Implementing for a KMS that supports ML-DSA
360///
361/// ```rust,ignore
362/// use a1::hybrid::{HybridSigner, HybridPublicKey, HybridSignature, SignatureAlgorithm};
363///
364/// struct MyHsmSigner { /* ... */ }
365///
366/// impl HybridSigner for MyHsmSigner {
367///     fn algorithm(&self) -> SignatureAlgorithm {
368///         SignatureAlgorithm::HybridMlDsa44Ed25519
369///     }
370///
371///     fn hybrid_verifying_key(&self) -> HybridPublicKey {
372///         HybridPublicKey {
373///             algorithm: SignatureAlgorithm::HybridMlDsa44Ed25519,
374///             classical_key: self.ed25519_vk(),
375///             pq_key_bytes: self.mldsa_pk_bytes(),
376///         }
377///     }
378///
379///     fn sign_hybrid(&self, msg: &[u8]) -> HybridSignature {
380///         let classical_sig = self.ed25519_sign(msg);
381///         let pq_sig_bytes  = self.mldsa_sign(msg);
382///         let pq_context = HybridSignature::compute_pq_context(
383///             self.algorithm(), msg, &pq_sig_bytes,
384///         );
385///         HybridSignature {
386///             algorithm: self.algorithm(),
387///             classical_sig,
388///             pq_sig_bytes,
389///             pq_context,
390///         }
391///     }
392/// }
393/// ```
394pub trait HybridSigner: Send + Sync {
395    fn algorithm(&self) -> SignatureAlgorithm;
396    fn hybrid_verifying_key(&self) -> HybridPublicKey;
397    fn sign_hybrid(&self, msg: &[u8]) -> HybridSignature;
398}
399
400// ── ClassicalHybridAdapter ────────────────────────────────────────────────────
401
402/// Wraps any `Signer` into a `HybridSigner` that emits Ed25519-tagged payloads.
403///
404/// Use this when migrating existing code to the `HybridSigner` interface
405/// without immediately activating a PQ backend. The output is wire-identical
406/// to a cert issued by a native `DyoloIdentity` but carries the structured
407/// `HybridSignature` envelope.
408///
409/// ```rust,ignore
410/// use a1::{DyoloIdentity};
411/// use a1::hybrid::ClassicalHybridAdapter;
412///
413/// let identity = DyoloIdentity::generate();
414/// let hybrid_signer = ClassicalHybridAdapter(&identity);
415/// let sig = hybrid_signer.sign_hybrid(msg);
416/// assert_eq!(sig.algorithm, SignatureAlgorithm::Ed25519);
417/// ```
418pub struct ClassicalHybridAdapter<'s, S: Signer>(pub &'s S);
419
420impl<S: Signer> HybridSigner for ClassicalHybridAdapter<'_, S> {
421    fn algorithm(&self) -> SignatureAlgorithm {
422        SignatureAlgorithm::Ed25519
423    }
424
425    fn hybrid_verifying_key(&self) -> HybridPublicKey {
426        HybridPublicKey::classical(self.0.verifying_key())
427    }
428
429    fn sign_hybrid(&self, msg: &[u8]) -> HybridSignature {
430        let classical_sig = self.0.sign_message(msg);
431        let pq_context = HybridSignature::compute_pq_context(SignatureAlgorithm::Ed25519, msg, &[]);
432        HybridSignature {
433            algorithm: SignatureAlgorithm::Ed25519,
434            classical_sig,
435            pq_sig_bytes: Vec::new(),
436            pq_context,
437        }
438    }
439}
440
441// ── Algorithm negotiation helper ──────────────────────────────────────────────
442
443/// Select the strongest algorithm that both the issuer and the environment support.
444///
445/// Returns the most secure algorithm from `candidates` that the current build
446/// can verify. With the `post-quantum` feature disabled, all hybrid candidates
447/// are reduced to `Ed25519` because full PQ verification is not available.
448///
449/// This function is deterministic and has no side effects. Use it during
450/// cert issuance to pick the appropriate algorithm for the deployment context.
451pub fn negotiate_algorithm(candidates: &[SignatureAlgorithm]) -> SignatureAlgorithm {
452    #[cfg(feature = "post-quantum")]
453    {
454        candidates
455            .iter()
456            .max_by_key(|a| a.as_u8())
457            .copied()
458            .unwrap_or(SignatureAlgorithm::Ed25519)
459    }
460    #[cfg(not(feature = "post-quantum"))]
461    {
462        let _ = candidates;
463        SignatureAlgorithm::Ed25519
464    }
465}
466
467// ── Serde helpers (hex encoding for byte blobs) ───────────────────────────────
468
469#[cfg(feature = "serde")]
470pub(crate) mod hex_bytes {
471    use serde::{Deserialize, Deserializer, Serializer};
472
473    pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
474        s.serialize_str(&hex::encode(v))
475    }
476
477    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
478        let s = String::deserialize(d)?;
479        if s.is_empty() {
480            return Ok(Vec::new());
481        }
482        hex::decode(&s).map_err(serde::de::Error::custom)
483    }
484}
485
486#[cfg(feature = "serde")]
487mod hex_32 {
488    use serde::{Deserialize, Deserializer, Serializer};
489
490    pub fn serialize<S: Serializer>(v: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
491        s.serialize_str(&hex::encode(v))
492    }
493
494    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
495        let s = String::deserialize(d)?;
496        let b = hex::decode(&s).map_err(serde::de::Error::custom)?;
497        b.try_into()
498            .map_err(|_| serde::de::Error::custom("expected 32-byte hex string"))
499    }
500}
501
502// ── Tests ─────────────────────────────────────────────────────────────────────
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507    use crate::identity::DyoloIdentity;
508
509    #[test]
510    fn algorithm_roundtrip() {
511        for v in [1u8, 2, 3] {
512            let alg = SignatureAlgorithm::from_u8(v).unwrap();
513            assert_eq!(alg.as_u8(), v);
514        }
515        assert!(SignatureAlgorithm::from_u8(0).is_err());
516        assert!(SignatureAlgorithm::from_u8(255).is_err());
517    }
518
519    #[test]
520    fn classical_adapter_verify() {
521        let id = DyoloIdentity::generate();
522        let adapter = ClassicalHybridAdapter(&id);
523        let msg = b"test-message-a1-hybrid";
524        let sig = adapter.sign_hybrid(msg);
525        let pk = adapter.hybrid_verifying_key();
526        assert!(sig.verify(msg, &pk).is_ok());
527    }
528
529    #[test]
530    fn pq_context_binding() {
531        let id = DyoloIdentity::generate();
532        let adapter = ClassicalHybridAdapter(&id);
533        let msg = b"a1-hybrid-context-test";
534        let mut sig = adapter.sign_hybrid(msg);
535        sig.pq_context[0] ^= 0x01;
536        let pk = adapter.hybrid_verifying_key();
537        assert!(sig.verify(msg, &pk).is_err());
538    }
539
540    #[test]
541    fn algorithm_mismatch_rejected() {
542        let id = DyoloIdentity::generate();
543        let adapter = ClassicalHybridAdapter(&id);
544        let msg = b"mismatch-test";
545        let sig = adapter.sign_hybrid(msg);
546        let mut pk = adapter.hybrid_verifying_key();
547        pk.algorithm = SignatureAlgorithm::HybridMlDsa44Ed25519;
548        assert!(sig.verify(msg, &pk).is_err());
549    }
550
551    #[test]
552    fn hybrid_public_key_commitment_distinct() {
553        let id = DyoloIdentity::generate();
554        let pk_ed = HybridPublicKey::classical(id.verifying_key());
555        let mut pk_hybrid = pk_ed.clone();
556        pk_hybrid.algorithm = SignatureAlgorithm::HybridMlDsa44Ed25519;
557        assert_ne!(pk_ed.commitment(), pk_hybrid.commitment());
558    }
559
560    #[test]
561    fn chain_algorithm_compatibility_uniform() {
562        let algs = vec![
563            SignatureAlgorithm::Ed25519,
564            SignatureAlgorithm::Ed25519,
565            SignatureAlgorithm::Ed25519,
566        ];
567        let compat = ChainAlgorithmCompatibility::from_algorithms(&algs).unwrap();
568        assert_eq!(
569            compat,
570            ChainAlgorithmCompatibility::Uniform(SignatureAlgorithm::Ed25519)
571        );
572    }
573
574    #[test]
575    fn chain_algorithm_compatibility_mixed_monotonic() {
576        let algs = vec![
577            SignatureAlgorithm::Ed25519,
578            SignatureAlgorithm::HybridMlDsa44Ed25519,
579            SignatureAlgorithm::HybridMlDsa44Ed25519,
580        ];
581        let compat = ChainAlgorithmCompatibility::from_algorithms(&algs).unwrap();
582        assert_eq!(
583            compat,
584            ChainAlgorithmCompatibility::MixedClassicalToHybrid {
585                classical_depth: 1,
586                hybrid_algorithm: SignatureAlgorithm::HybridMlDsa44Ed25519,
587            }
588        );
589    }
590
591    #[test]
592    fn chain_algorithm_compatibility_non_monotonic_rejected() {
593        let algs = vec![
594            SignatureAlgorithm::HybridMlDsa44Ed25519,
595            SignatureAlgorithm::Ed25519,
596        ];
597        assert!(ChainAlgorithmCompatibility::from_algorithms(&algs).is_err());
598    }
599
600    #[test]
601    fn negotiate_algorithm_defaults_to_ed25519_without_pq_feature() {
602        let candidates = vec![
603            SignatureAlgorithm::Ed25519,
604            SignatureAlgorithm::HybridMlDsa44Ed25519,
605        ];
606        let chosen = negotiate_algorithm(&candidates);
607        #[cfg(not(feature = "post-quantum"))]
608        assert_eq!(chosen, SignatureAlgorithm::Ed25519);
609        #[cfg(feature = "post-quantum")]
610        assert_eq!(chosen, SignatureAlgorithm::HybridMlDsa44Ed25519);
611    }
612
613    #[test]
614    fn pq_size_constants() {
615        assert_eq!(SignatureAlgorithm::Ed25519.pq_public_key_len(), 0);
616        assert_eq!(
617            SignatureAlgorithm::HybridMlDsa44Ed25519.pq_public_key_len(),
618            1312
619        );
620        assert_eq!(
621            SignatureAlgorithm::HybridMlDsa65Ed25519.pq_public_key_len(),
622            1952
623        );
624        assert_eq!(
625            SignatureAlgorithm::HybridMlDsa44Ed25519.pq_signature_len(),
626            2420
627        );
628        assert_eq!(
629            SignatureAlgorithm::HybridMlDsa65Ed25519.pq_signature_len(),
630            3309
631        );
632    }
633}