1use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11use crate::mesh::{MeshError, MeshStream};
12
13#[async_trait]
25pub trait EncryptedChannel: Send + Sync + 'static {
26 async fn send_encrypted(&mut self, plaintext: &[u8]) -> Result<(), MeshError>;
28
29 async fn recv_encrypted(&mut self) -> Result<Vec<u8>, MeshError>;
33
34 fn remote_static_key(&self) -> Option<&[u8]>;
36
37 fn handshake_hash(&self) -> Option<&[u8]> {
42 None
43 }
44
45 async fn close(&mut self) -> Result<(), MeshError>;
47}
48
49#[non_exhaustive]
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52pub enum NoisePattern {
53 XX,
55 IK,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct EncryptedPeer {
67 pub node_id: String,
69 pub remote_static_key: Vec<u8>,
71 pub pattern: NoisePattern,
73 pub kem_upgraded: bool,
75 pub established_at_unix_us: u64,
77 pub address: Option<String>,
79}
80
81impl EncryptedPeer {
82 pub fn new(
84 node_id: impl Into<String>,
85 remote_static_key: Vec<u8>,
86 pattern: NoisePattern,
87 established_at_unix_us: u64,
88 ) -> Self {
89 Self {
90 node_id: node_id.into(),
91 remote_static_key,
92 pattern,
93 kem_upgraded: false,
94 established_at_unix_us,
95 address: None,
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct NoiseConfig {
103 pub pattern: NoisePattern,
105
106 pub local_private_key: [u8; 32],
108
109 pub remote_static_key: Option<[u8; 32]>,
111}
112
113pub struct NoiseChannel {
121 stream: Box<dyn MeshStream>,
122 transport: snow::TransportState,
123 remote_static: Option<Vec<u8>>,
124 handshake_hash: Vec<u8>,
125}
126
127impl NoiseChannel {
128 pub fn handshake_hash(&self) -> &[u8] {
131 &self.handshake_hash
132 }
133
134 pub async fn initiate(
136 mut stream: Box<dyn MeshStream>,
137 config: &NoiseConfig,
138 ) -> Result<Self, MeshError> {
139 let builder = snow::Builder::new(
140 "Noise_XX_25519_ChaChaPoly_SHA256"
141 .parse()
142 .map_err(|e| MeshError::Handshake(format!("bad noise params: {e}")))?,
143 )
144 .local_private_key(&config.local_private_key)
145 .map_err(|e| MeshError::Handshake(format!("bad private key: {e}")))?;
146
147 let mut handshake = builder
148 .build_initiator()
149 .map_err(|e| MeshError::Handshake(format!("build initiator: {e}")))?;
150
151 let mut buf = vec![0u8; 65535];
152
153 let len = handshake
157 .write_message(&[], &mut buf)
158 .map_err(|e| MeshError::Handshake(format!("write msg 1: {e}")))?;
159 stream.send(&buf[..len]).await?;
160
161 let msg = stream.recv().await?;
163 handshake
164 .read_message(&msg, &mut buf)
165 .map_err(|e| MeshError::Handshake(format!("read msg 2: {e}")))?;
166
167 let len = handshake
169 .write_message(&[], &mut buf)
170 .map_err(|e| MeshError::Handshake(format!("write msg 3: {e}")))?;
171 stream.send(&buf[..len]).await?;
172
173 let remote_static = handshake.get_remote_static().map(|k| k.to_vec());
174 let handshake_hash = handshake.get_handshake_hash().to_vec();
175
176 let transport = handshake
177 .into_transport_mode()
178 .map_err(|e| MeshError::Handshake(format!("transport mode: {e}")))?;
179
180 tracing::info!("noise XX handshake complete (initiator)");
181
182 Ok(Self {
183 stream,
184 transport,
185 remote_static,
186 handshake_hash,
187 })
188 }
189
190 pub async fn respond(
192 mut stream: Box<dyn MeshStream>,
193 config: &NoiseConfig,
194 ) -> Result<Self, MeshError> {
195 let builder = snow::Builder::new(
196 "Noise_XX_25519_ChaChaPoly_SHA256"
197 .parse()
198 .map_err(|e| MeshError::Handshake(format!("bad noise params: {e}")))?,
199 )
200 .local_private_key(&config.local_private_key)
201 .map_err(|e| MeshError::Handshake(format!("bad private key: {e}")))?;
202
203 let mut handshake = builder
204 .build_responder()
205 .map_err(|e| MeshError::Handshake(format!("build responder: {e}")))?;
206
207 let mut buf = vec![0u8; 65535];
208
209 let msg = stream.recv().await?;
213 handshake
214 .read_message(&msg, &mut buf)
215 .map_err(|e| MeshError::Handshake(format!("read msg 1: {e}")))?;
216
217 let len = handshake
219 .write_message(&[], &mut buf)
220 .map_err(|e| MeshError::Handshake(format!("write msg 2: {e}")))?;
221 stream.send(&buf[..len]).await?;
222
223 let msg = stream.recv().await?;
225 handshake
226 .read_message(&msg, &mut buf)
227 .map_err(|e| MeshError::Handshake(format!("read msg 3: {e}")))?;
228
229 let remote_static = handshake.get_remote_static().map(|k| k.to_vec());
230 let handshake_hash = handshake.get_handshake_hash().to_vec();
231
232 let transport = handshake
233 .into_transport_mode()
234 .map_err(|e| MeshError::Handshake(format!("transport mode: {e}")))?;
235
236 tracing::info!("noise XX handshake complete (responder)");
237
238 Ok(Self {
239 stream,
240 transport,
241 remote_static,
242 handshake_hash,
243 })
244 }
245}
246
247#[async_trait]
248impl EncryptedChannel for NoiseChannel {
249 async fn send_encrypted(&mut self, plaintext: &[u8]) -> Result<(), MeshError> {
250 let mut buf = vec![0u8; plaintext.len() + 16 + 2]; let len = self
252 .transport
253 .write_message(plaintext, &mut buf)
254 .map_err(|e| MeshError::Io(format!("encrypt: {e}")))?;
255 self.stream.send(&buf[..len]).await
256 }
257
258 async fn recv_encrypted(&mut self) -> Result<Vec<u8>, MeshError> {
259 let ciphertext = self.stream.recv().await?;
260 let mut buf = vec![0u8; ciphertext.len()];
261 let len = self
262 .transport
263 .read_message(&ciphertext, &mut buf)
264 .map_err(|e| MeshError::Io(format!("decrypt: {e}")))?;
265 buf.truncate(len);
266 Ok(buf)
267 }
268
269 fn remote_static_key(&self) -> Option<&[u8]> {
270 self.remote_static.as_deref()
271 }
272
273 fn handshake_hash(&self) -> Option<&[u8]> {
274 Some(&self.handshake_hash)
275 }
276
277 async fn close(&mut self) -> Result<(), MeshError> {
278 self.stream.close().await
279 }
280}
281
282pub struct PassthroughChannel {
289 stream: Box<dyn MeshStream>,
290}
291
292impl PassthroughChannel {
293 pub fn new(stream: Box<dyn MeshStream>) -> Self {
295 Self { stream }
296 }
297}
298
299#[async_trait]
300impl EncryptedChannel for PassthroughChannel {
301 async fn send_encrypted(&mut self, plaintext: &[u8]) -> Result<(), MeshError> {
302 self.stream.send(plaintext).await
303 }
304
305 async fn recv_encrypted(&mut self) -> Result<Vec<u8>, MeshError> {
306 self.stream.recv().await
307 }
308
309 fn remote_static_key(&self) -> Option<&[u8]> {
310 None
311 }
312
313 async fn close(&mut self) -> Result<(), MeshError> {
314 self.stream.close().await
315 }
316}
317
318pub fn noise_static_public(private: &[u8; 32]) -> Option<[u8; 32]> {
323 use snow::resolvers::{CryptoResolver, DefaultResolver};
324 let mut dh = DefaultResolver.resolve_dh(&snow::params::DHChoice::Curve25519)?;
325 dh.set(private);
326 dh.pubkey().try_into().ok()
327}
328
329pub async fn create_encrypted_channel(
334 stream: Box<dyn MeshStream>,
335 noise_config: Option<&NoiseConfig>,
336 is_initiator: bool,
337) -> Result<Box<dyn EncryptedChannel>, MeshError> {
338 match noise_config {
339 Some(config) => {
340 let channel = if is_initiator {
341 NoiseChannel::initiate(stream, config).await?
342 } else {
343 NoiseChannel::respond(stream, config).await?
344 };
345 Ok(Box::new(channel))
346 }
347 None => Ok(Box::new(PassthroughChannel::new(stream))),
348 }
349}
350
351#[derive(Debug, Clone)]
355pub struct KeyRotationState {
356 pub generation: u64,
358 pub established_at: std::time::Instant,
360 pub max_lifetime: std::time::Duration,
362 pub grace_period: std::time::Duration,
364 pub rotating: bool,
366}
367
368impl KeyRotationState {
369 pub fn new(max_lifetime: std::time::Duration, grace_period: std::time::Duration) -> Self {
371 Self {
372 generation: 0,
373 established_at: std::time::Instant::now(),
374 max_lifetime,
375 grace_period,
376 rotating: false,
377 }
378 }
379
380 pub fn needs_rotation(&self) -> bool {
382 self.established_at.elapsed() >= self.max_lifetime
386 }
387
388 pub fn begin_rotation(&mut self) {
390 self.rotating = true;
391 }
392
393 pub fn complete_rotation(&mut self) {
395 self.generation += 1;
396 self.established_at = std::time::Instant::now();
397 self.rotating = false;
398 }
399
400 pub fn old_key_valid(&self) -> bool {
403 self.rotating || self.established_at.elapsed() < self.grace_period
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct KemConfig {
412 pub kem_supported: bool,
414 pub kem_public_key: Option<Vec<u8>>,
416}
417
418#[non_exhaustive]
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
421pub enum KemNegotiationResult {
422 BothSupported,
424 GracefulDegradation,
426 ClassicalOnly,
428}
429
430pub fn negotiate_kem(local: &KemConfig, remote: &KemConfig) -> KemNegotiationResult {
432 match (local.kem_supported, remote.kem_supported) {
433 (true, true) => KemNegotiationResult::BothSupported,
434 (true, false) | (false, true) => KemNegotiationResult::GracefulDegradation,
435 (false, false) => KemNegotiationResult::ClassicalOnly,
436 }
437}
438
439#[derive(Debug, Clone)]
441pub struct HybridKeyExchange {
442 pub classical_secret: Vec<u8>,
444 pub pq_secret: Option<Vec<u8>>,
446 pub final_key: Vec<u8>,
448 pub pq_active: bool,
450}
451
452impl HybridKeyExchange {
453 pub fn combine(classical: Vec<u8>, pq: Option<Vec<u8>>) -> Self {
459 use sha2::{Digest, Sha256};
460 let mut hasher = Sha256::new();
461 hasher.update(&classical);
462 if let Some(ref pq_s) = pq {
463 hasher.update(pq_s);
464 }
465 hasher.update(b"weftos-hybrid-kem-v1");
466 let final_key = hasher.finalize().to_vec();
467
468 Self {
469 pq_active: pq.is_some(),
470 classical_secret: classical,
471 pq_secret: pq,
472 final_key,
473 }
474 }
475}
476
477pub struct KemUpgradeProtocol {
484 pub local: KemConfig,
486 pub remote: KemConfig,
488 pub result: KemNegotiationResult,
490}
491
492impl KemUpgradeProtocol {
493 pub fn new(local: KemConfig, remote: KemConfig) -> Self {
495 let result = negotiate_kem(&local, &remote);
496 Self {
497 local,
498 remote,
499 result,
500 }
501 }
502
503 pub fn requires_upgrade(&self) -> bool {
508 self.result == KemNegotiationResult::BothSupported
509 }
510
511 pub fn execute(&self, classical_secret: Vec<u8>) -> HybridKeyExchange {
518 match self.result {
519 KemNegotiationResult::BothSupported => {
520 let pq_secret = {
523 use sha2::{Digest, Sha256};
524 let mut h = Sha256::new();
525 h.update(b"ml-kem-768-simulated");
526 if let Some(ref pk) = self.remote.kem_public_key {
527 h.update(pk);
528 }
529 h.finalize().to_vec()
530 };
531 HybridKeyExchange::combine(classical_secret, Some(pq_secret))
532 }
533 _ => HybridKeyExchange::combine(classical_secret, None),
534 }
535 }
536}
537
538#[cfg(test)]
541mod mock {
542 use std::collections::VecDeque;
543 use std::net::SocketAddr;
544 use std::sync::{Arc, Mutex};
545
546 use async_trait::async_trait;
547
548 use crate::mesh::{MeshError, MeshStream};
549
550 #[derive(Debug, Default)]
552 pub struct MockBuffer {
553 pub data: VecDeque<Vec<u8>>,
554 }
555
556 pub struct MockStream {
558 pub tx: Arc<Mutex<MockBuffer>>,
560 pub rx: Arc<Mutex<MockBuffer>>,
562 pub closed: bool,
563 }
564
565 impl MockStream {
566 pub fn pair() -> (Self, Self) {
568 let a_to_b = Arc::new(Mutex::new(MockBuffer::default()));
569 let b_to_a = Arc::new(Mutex::new(MockBuffer::default()));
570
571 let a = MockStream {
572 tx: Arc::clone(&a_to_b),
573 rx: Arc::clone(&b_to_a),
574 closed: false,
575 };
576 let b = MockStream {
577 tx: b_to_a,
578 rx: a_to_b,
579 closed: false,
580 };
581 (a, b)
582 }
583 }
584
585 #[async_trait]
586 impl MeshStream for MockStream {
587 async fn send(&mut self, data: &[u8]) -> Result<(), MeshError> {
588 if self.closed {
589 return Err(MeshError::ConnectionClosed);
590 }
591 self.tx.lock().unwrap().data.push_back(data.to_vec());
592 Ok(())
593 }
594
595 async fn recv(&mut self) -> Result<Vec<u8>, MeshError> {
596 if self.closed {
597 return Err(MeshError::ConnectionClosed);
598 }
599 self.rx
600 .lock()
601 .unwrap()
602 .data
603 .pop_front()
604 .ok_or(MeshError::ConnectionClosed)
605 }
606
607 async fn close(&mut self) -> Result<(), MeshError> {
608 self.closed = true;
609 Ok(())
610 }
611
612 fn remote_addr(&self) -> Option<SocketAddr> {
613 Some("127.0.0.1:0".parse().unwrap())
614 }
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::mock::MockStream;
621 use super::*;
622
623 #[test]
624 fn noise_pattern_variants() {
625 assert_eq!(NoisePattern::XX, NoisePattern::XX);
626 assert_ne!(NoisePattern::XX, NoisePattern::IK);
627 }
628
629 #[test]
630 fn noise_pattern_serde_roundtrip() {
631 for pattern in [NoisePattern::XX, NoisePattern::IK] {
632 let json = serde_json::to_string(&pattern).unwrap();
633 let restored: NoisePattern = serde_json::from_str(&json).unwrap();
634 assert_eq!(restored, pattern);
635 }
636 }
637
638 #[test]
639 fn encrypted_peer_serde_roundtrip() {
640 let peer = EncryptedPeer {
641 node_id: "node-a".into(),
642 remote_static_key: vec![1, 2, 3, 4],
643 pattern: NoisePattern::XX,
644 kem_upgraded: true,
645 established_at_unix_us: 1_700_000_000_000_000,
646 address: Some("127.0.0.1:9489".into()),
647 };
648 let json = serde_json::to_string(&peer).unwrap();
649 let restored: EncryptedPeer = serde_json::from_str(&json).unwrap();
650 assert_eq!(restored.node_id, "node-a");
651 assert_eq!(restored.remote_static_key, vec![1, 2, 3, 4]);
652 assert_eq!(restored.pattern, NoisePattern::XX);
653 assert!(restored.kem_upgraded);
654 assert_eq!(restored.established_at_unix_us, 1_700_000_000_000_000);
655 assert_eq!(restored.address.as_deref(), Some("127.0.0.1:9489"));
656 }
657
658 #[test]
659 fn noise_config_fields() {
660 let cfg = NoiseConfig {
661 pattern: NoisePattern::XX,
662 local_private_key: [0x42; 32],
663 remote_static_key: None,
664 };
665 assert_eq!(cfg.pattern, NoisePattern::XX);
666 assert_eq!(cfg.local_private_key, [0x42; 32]);
667 assert!(cfg.remote_static_key.is_none());
668
669 let cfg_ik = NoiseConfig {
670 pattern: NoisePattern::IK,
671 local_private_key: [1; 32],
672 remote_static_key: Some([2; 32]),
673 };
674 assert_eq!(cfg_ik.pattern, NoisePattern::IK);
675 assert_eq!(cfg_ik.remote_static_key, Some([2; 32]));
676 }
677
678 #[tokio::test]
679 async fn passthrough_send_recv() {
680 let (a, b) = MockStream::pair();
681 let mut chan_a = PassthroughChannel::new(Box::new(a));
682 let mut chan_b = PassthroughChannel::new(Box::new(b));
683
684 chan_a.send_encrypted(b"hello mesh").await.unwrap();
685 let received = chan_b.recv_encrypted().await.unwrap();
686 assert_eq!(received, b"hello mesh");
687 }
688
689 #[tokio::test]
690 async fn passthrough_bidirectional() {
691 let (a, b) = MockStream::pair();
692 let mut chan_a = PassthroughChannel::new(Box::new(a));
693 let mut chan_b = PassthroughChannel::new(Box::new(b));
694
695 chan_a.send_encrypted(b"ping").await.unwrap();
696 let msg = chan_b.recv_encrypted().await.unwrap();
697 assert_eq!(msg, b"ping");
698
699 chan_b.send_encrypted(b"pong").await.unwrap();
700 let msg = chan_a.recv_encrypted().await.unwrap();
701 assert_eq!(msg, b"pong");
702 }
703
704 #[test]
705 fn passthrough_no_remote_key() {
706 let (a, _b) = MockStream::pair();
707 let chan = PassthroughChannel::new(Box::new(a));
708 assert!(chan.remote_static_key().is_none());
709 }
710
711 #[test]
714 fn key_rotation_initial_state() {
715 let state = KeyRotationState::new(
716 std::time::Duration::from_secs(3600),
717 std::time::Duration::from_secs(60),
718 );
719 assert_eq!(state.generation, 0);
720 assert!(!state.rotating);
721 assert!(!state.needs_rotation());
722 }
723
724 #[test]
725 fn key_rotation_needs_rotation_after_lifetime() {
726 let mut state = KeyRotationState::new(
727 std::time::Duration::from_secs(0), std::time::Duration::from_secs(60),
729 );
730 assert!(state.needs_rotation());
732 assert!(state.old_key_valid());
734
735 state.begin_rotation();
736 assert!(state.rotating);
737 assert!(state.old_key_valid()); }
739
740 #[test]
741 fn key_rotation_complete_lifecycle() {
742 let mut state = KeyRotationState::new(
743 std::time::Duration::from_secs(0),
744 std::time::Duration::from_secs(3600), );
746 assert_eq!(state.generation, 0);
747
748 state.begin_rotation();
749 assert!(state.rotating);
750
751 state.complete_rotation();
752 assert_eq!(state.generation, 1);
753 assert!(!state.rotating);
754 assert!(state.old_key_valid());
756 }
757
758 #[test]
759 fn key_rotation_multiple_generations() {
760 let mut state = KeyRotationState::new(
761 std::time::Duration::from_secs(0),
762 std::time::Duration::from_secs(3600),
763 );
764 for i in 0..5 {
765 assert_eq!(state.generation, i);
766 state.begin_rotation();
767 state.complete_rotation();
768 }
769 assert_eq!(state.generation, 5);
770 }
771
772 #[tokio::test]
773 async fn passthrough_close() {
774 let (a, _b) = MockStream::pair();
775 let mut chan = PassthroughChannel::new(Box::new(a));
776 chan.close().await.unwrap();
777 let result = chan.send_encrypted(b"after close").await;
779 assert!(result.is_err());
780 }
781
782 #[test]
785 fn kem_negotiation_both_supported() {
786 let local = KemConfig {
787 kem_supported: true,
788 kem_public_key: Some(vec![0xAA; 1184]),
789 };
790 let remote = KemConfig {
791 kem_supported: true,
792 kem_public_key: Some(vec![0xBB; 1184]),
793 };
794 assert_eq!(
795 negotiate_kem(&local, &remote),
796 KemNegotiationResult::BothSupported,
797 );
798 }
799
800 #[test]
801 fn kem_negotiation_graceful_degradation() {
802 let local = KemConfig {
804 kem_supported: true,
805 kem_public_key: Some(vec![0xAA; 1184]),
806 };
807 let remote = KemConfig {
808 kem_supported: false,
809 kem_public_key: None,
810 };
811 assert_eq!(
812 negotiate_kem(&local, &remote),
813 KemNegotiationResult::GracefulDegradation,
814 );
815
816 assert_eq!(
818 negotiate_kem(&remote, &local),
819 KemNegotiationResult::GracefulDegradation,
820 );
821 }
822
823 #[test]
824 fn kem_negotiation_classical_only() {
825 let local = KemConfig {
826 kem_supported: false,
827 kem_public_key: None,
828 };
829 let remote = KemConfig {
830 kem_supported: false,
831 kem_public_key: None,
832 };
833 assert_eq!(
834 negotiate_kem(&local, &remote),
835 KemNegotiationResult::ClassicalOnly,
836 );
837 }
838
839 #[test]
840 fn hybrid_key_exchange_with_pq() {
841 let classical = vec![0x01; 32];
842 let pq = vec![0x02; 32];
843 let hke = HybridKeyExchange::combine(classical.clone(), Some(pq.clone()));
844
845 assert!(hke.pq_active, "PQ should be active");
846 assert_eq!(hke.classical_secret, classical);
847 assert_eq!(hke.pq_secret.as_ref().unwrap(), &pq);
848 assert_eq!(hke.final_key.len(), 32, "SHA-256 output is 32 bytes");
849
850 let classical_only = HybridKeyExchange::combine(classical.clone(), None);
852 assert_ne!(
853 hke.final_key, classical_only.final_key,
854 "hybrid key must differ from classical-only key"
855 );
856 }
857
858 #[test]
859 fn hybrid_key_exchange_classical_only() {
860 let classical = vec![0x01; 32];
861 let hke = HybridKeyExchange::combine(classical.clone(), None);
862
863 assert!(!hke.pq_active, "PQ should not be active");
864 assert!(hke.pq_secret.is_none());
865 assert_eq!(hke.final_key.len(), 32);
866 }
867
868 #[test]
869 fn kem_upgrade_requires_upgrade_when_both_support() {
870 let local = KemConfig {
871 kem_supported: true,
872 kem_public_key: Some(vec![0xAA; 1184]),
873 };
874 let remote = KemConfig {
875 kem_supported: true,
876 kem_public_key: Some(vec![0xBB; 1184]),
877 };
878 let protocol = KemUpgradeProtocol::new(local, remote);
879 assert!(
880 protocol.requires_upgrade(),
881 "must require upgrade when both sides support KEM"
882 );
883 assert_eq!(protocol.result, KemNegotiationResult::BothSupported);
884 }
885
886 #[test]
887 fn kem_upgrade_no_upgrade_when_degraded() {
888 let local = KemConfig {
889 kem_supported: true,
890 kem_public_key: Some(vec![0xAA; 1184]),
891 };
892 let remote = KemConfig {
893 kem_supported: false,
894 kem_public_key: None,
895 };
896 let protocol = KemUpgradeProtocol::new(local, remote);
897 assert!(
898 !protocol.requires_upgrade(),
899 "should not require upgrade with graceful degradation"
900 );
901 }
902
903 #[test]
904 fn kem_upgrade_before_sync_streams() {
905 let local = KemConfig {
908 kem_supported: true,
909 kem_public_key: Some(vec![0xAA; 1184]),
910 };
911 let remote = KemConfig {
912 kem_supported: true,
913 kem_public_key: Some(vec![0xBB; 1184]),
914 };
915 let protocol = KemUpgradeProtocol::new(local, remote);
916 assert!(protocol.requires_upgrade());
917
918 let classical_secret = vec![0xCC; 32];
919 let hke = protocol.execute(classical_secret.clone());
920
921 assert!(hke.pq_active, "PQ must be active after KEM upgrade");
922 assert!(hke.pq_secret.is_some(), "PQ secret must be present");
923 assert_eq!(hke.final_key.len(), 32);
924
925 let classical_only = HybridKeyExchange::combine(classical_secret, None);
927 assert_ne!(
928 hke.final_key, classical_only.final_key,
929 "hybrid final key must differ from classical-only"
930 );
931 }
932
933 #[test]
934 fn kem_upgrade_classical_fallback() {
935 let local = KemConfig {
937 kem_supported: false,
938 kem_public_key: None,
939 };
940 let remote = KemConfig {
941 kem_supported: false,
942 kem_public_key: None,
943 };
944 let protocol = KemUpgradeProtocol::new(local, remote);
945 assert!(!protocol.requires_upgrade());
946
947 let classical_secret = vec![0xCC; 32];
948 let hke = protocol.execute(classical_secret);
949
950 assert!(!hke.pq_active);
951 assert!(hke.pq_secret.is_none());
952 }
953
954 #[test]
955 fn kem_config_serde_roundtrip() {
956 let cfg = KemConfig {
957 kem_supported: true,
958 kem_public_key: Some(vec![0x42; 1184]),
959 };
960 let json = serde_json::to_string(&cfg).unwrap();
961 let restored: KemConfig = serde_json::from_str(&json).unwrap();
962 assert!(restored.kem_supported);
963 assert_eq!(restored.kem_public_key.unwrap().len(), 1184);
964 }
965
966 #[test]
967 fn kem_negotiation_result_serde_roundtrip() {
968 for result in [
969 KemNegotiationResult::BothSupported,
970 KemNegotiationResult::GracefulDegradation,
971 KemNegotiationResult::ClassicalOnly,
972 ] {
973 let json = serde_json::to_string(&result).unwrap();
974 let restored: KemNegotiationResult = serde_json::from_str(&json).unwrap();
975 assert_eq!(restored, result);
976 }
977 }
978}