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#[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 #[default]
41 Ed25519 = 1,
42
43 HybridMlDsa44Ed25519 = 2,
49
50 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 #[inline]
74 pub fn requires_pq(self) -> bool {
75 matches!(
76 self,
77 Self::HybridMlDsa44Ed25519 | Self::HybridMlDsa65Ed25519
78 )
79 }
80
81 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ChainAlgorithmCompatibility {
131 Uniform(SignatureAlgorithm),
133
134 MixedClassicalToHybrid {
139 classical_depth: usize,
140 hybrid_algorithm: SignatureAlgorithm,
141 },
142}
143
144impl ChainAlgorithmCompatibility {
145 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#[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 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 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 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#[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 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
351pub 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
400pub 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
441pub 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#[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#[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}