Skip to main content

clawft_kernel/
mesh_noise.rs

1//! Noise Protocol encryption for mesh transport (K6.1).
2//!
3//! Wraps raw [`MeshStream`] with Noise XX or IK handshake encryption.
4//! The actual `snow` integration is planned for a future milestone;
5//! this module provides the trait contract and a passthrough
6//! implementation for testing.
7
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11use crate::mesh::{MeshError, MeshStream};
12
13/// Encrypted channel wrapping a raw mesh stream.
14///
15/// After a Noise handshake completes, all traffic flows through
16/// this interface. Each `send_encrypted` / `recv_encrypted` call
17/// handles framing, encryption, and authentication transparently.
18///
19/// # Cancel-safety (ADR-010 / WEFT-18)
20///
21/// `recv_encrypted` is raced in `tokio::select!` (mesh peer loop). It
22/// must be cancel-safe: implementors delegate to [`MeshStream::recv`],
23/// which itself must retain partial frame progress across cancellation.
24#[async_trait]
25pub trait EncryptedChannel: Send + Sync + 'static {
26    /// Send encrypted data (plaintext in, ciphertext on the wire).
27    async fn send_encrypted(&mut self, plaintext: &[u8]) -> Result<(), MeshError>;
28
29    /// Receive and decrypt data (ciphertext from wire, plaintext out).
30    ///
31    /// Cancel-safe: may be dropped while pending inside `tokio::select!`.
32    async fn recv_encrypted(&mut self) -> Result<Vec<u8>, MeshError>;
33
34    /// Get the remote peer's static public key (after handshake).
35    fn remote_static_key(&self) -> Option<&[u8]>;
36
37    /// The Noise handshake hash of this session, when the channel has
38    /// one. Both sides of a session compute the same value; admission
39    /// signs it to bind a node identity to this exact session (P3-K1).
40    /// `None` for channels without a handshake (plaintext passthrough).
41    fn handshake_hash(&self) -> Option<&[u8]> {
42        None
43    }
44
45    /// Close the encrypted channel gracefully.
46    async fn close(&mut self) -> Result<(), MeshError>;
47}
48
49/// Noise handshake pattern.
50#[non_exhaustive]
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52pub enum NoisePattern {
53    /// XX pattern: mutual authentication, first contact.
54    XX,
55    /// IK pattern: known responder key, 1-RTT.
56    IK,
57}
58
59/// Established encrypted peer session metadata (WEFT-115).
60///
61/// Holds the identity and crypto session facts after Noise completes.
62/// The live byte channel is [`EncryptedChannel`] / [`NoiseChannel`];
63/// this struct is the serialisable session descriptor used by pools,
64/// diagnostics, and protocol documentation.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct EncryptedPeer {
67    /// Remote node identity.
68    pub node_id: String,
69    /// Remote static public key (Noise), hex or raw-length bytes as vec.
70    pub remote_static_key: Vec<u8>,
71    /// Handshake pattern used to establish the session.
72    pub pattern: NoisePattern,
73    /// Whether a post-quantum KEM upgrade was negotiated.
74    pub kem_upgraded: bool,
75    /// Wall-clock microseconds when the session was established.
76    pub established_at_unix_us: u64,
77    /// Peer socket address string (if known).
78    pub address: Option<String>,
79}
80
81impl EncryptedPeer {
82    /// Build session metadata from a completed channel + identity.
83    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/// Configuration for a Noise handshake.
101#[derive(Debug, Clone)]
102pub struct NoiseConfig {
103    /// Which handshake pattern to use.
104    pub pattern: NoisePattern,
105
106    /// Local Ed25519 private key bytes (32 bytes).
107    pub local_private_key: [u8; 32],
108
109    /// Remote static public key (required for IK, optional for XX).
110    pub remote_static_key: Option<[u8; 32]>,
111}
112
113// ── Snow-backed Noise Protocol implementation ───────────────────
114
115/// Real Noise Protocol encrypted channel using `snow`.
116///
117/// Performs a Noise XX handshake over the underlying `MeshStream`,
118/// then encrypts/decrypts all subsequent messages using the established
119/// session keys. Uses `Noise_XX_25519_ChaChaPoly_SHA256`.
120pub 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    /// The session's Noise handshake hash (32 bytes for SHA-256). Equal
129    /// on both ends of one session, different on every other session.
130    pub fn handshake_hash(&self) -> &[u8] {
131        &self.handshake_hash
132    }
133
134    /// Perform a Noise XX handshake as the initiator and return an encrypted channel.
135    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        // XX pattern: initiator sends → responder sends → initiator sends
154
155        // -> e
156        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        // <- e, ee, s, es
162        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        // -> s, se
168        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    /// Perform a Noise XX handshake as the responder and return an encrypted channel.
191    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        // XX pattern: initiator sends → responder sends → initiator sends
210
211        // <- e
212        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        // -> e, ee, s, es
218        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        // <- s, se
224        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]; // AEAD tag + length prefix room
251        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
282// ── Passthrough (test/dev) implementation ────────────────────────
283
284/// Passthrough "encryption" for testing and development.
285///
286/// No actual crypto — plaintext passes through unchanged.
287/// Use `NoiseChannel` for production deployments.
288pub struct PassthroughChannel {
289    stream: Box<dyn MeshStream>,
290}
291
292impl PassthroughChannel {
293    /// Wrap a raw stream with passthrough (no-op) encryption.
294    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
318/// The Noise static public key for a static private key (Curve25519),
319/// i.e. what the remote side sees as this node's remote static. Needed to
320/// fill `noise_static_pub` in an admission hello on the initiator side,
321/// where the handshake does not expose the local static.
322pub 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
329/// Create an encrypted channel for a mesh connection.
330///
331/// When `noise_config` is `Some`, performs a real Noise XX handshake.
332/// When `None`, returns a passthrough (plaintext) channel.
333pub 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// ── Key rotation protocol ───────────────────────────────────────
352
353/// Key rotation state for a mesh connection.
354#[derive(Debug, Clone)]
355pub struct KeyRotationState {
356    /// Current key generation number.
357    pub generation: u64,
358    /// When the current key was established.
359    pub established_at: std::time::Instant,
360    /// Maximum key lifetime before rotation.
361    pub max_lifetime: std::time::Duration,
362    /// Grace period: old key accepted during transition.
363    pub grace_period: std::time::Duration,
364    /// Whether rotation is in progress.
365    pub rotating: bool,
366}
367
368impl KeyRotationState {
369    /// Create a new key rotation state at generation 0.
370    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    /// Check if key rotation is needed based on elapsed lifetime.
381    pub fn needs_rotation(&self) -> bool {
382        // `>=` so a zero lifetime (and the exact-deadline instant) needs
383        // rotation — `>` left a sub-tick race where a freshly established
384        // zero-lifetime key read as not-yet-due under parallel load.
385        self.established_at.elapsed() >= self.max_lifetime
386    }
387
388    /// Start key rotation (marks as in-progress).
389    pub fn begin_rotation(&mut self) {
390        self.rotating = true;
391    }
392
393    /// Complete key rotation, advancing to the next generation.
394    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    /// Check if the old key (previous generation) is still valid.
401    /// True during active rotation or within the grace period.
402    pub fn old_key_valid(&self) -> bool {
403        self.rotating || self.established_at.elapsed() < self.grace_period
404    }
405}
406
407// ── Post-quantum KEM types and hybrid handshake protocol ──────────
408
409/// Post-quantum Key Encapsulation Mechanism (KEM) configuration.
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct KemConfig {
412    /// Whether this node supports ML-KEM-768.
413    pub kem_supported: bool,
414    /// KEM public key (1184 bytes for ML-KEM-768).
415    pub kem_public_key: Option<Vec<u8>>,
416}
417
418/// Result of KEM capability negotiation between two peers.
419#[non_exhaustive]
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
421pub enum KemNegotiationResult {
422    /// Both sides support KEM -- full PQ protection.
423    BothSupported,
424    /// One side lacks KEM -- graceful fallback to classical.
425    GracefulDegradation,
426    /// Neither supports KEM -- classical only.
427    ClassicalOnly,
428}
429
430/// Negotiate KEM capability between two peers.
431pub 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/// Result of a hybrid key exchange (classical Noise XX + optional ML-KEM-768).
440#[derive(Debug, Clone)]
441pub struct HybridKeyExchange {
442    /// Classical shared secret (from Noise XX X25519 DH).
443    pub classical_secret: Vec<u8>,
444    /// Post-quantum shared secret (from ML-KEM-768, if both sides support).
445    pub pq_secret: Option<Vec<u8>>,
446    /// Final combined key: SHA-256(classical || pq || "weftos-hybrid-kem-v1").
447    pub final_key: Vec<u8>,
448    /// Whether PQ was used.
449    pub pq_active: bool,
450}
451
452impl HybridKeyExchange {
453    /// Combine classical and PQ secrets into a final hybrid key.
454    ///
455    /// The combination is: `SHA-256(classical || pq || "weftos-hybrid-kem-v1")`.
456    /// When `pq` is `None`, the domain tag still differentiates the output
457    /// from a raw classical key.
458    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
477/// KEM upgrade protocol: runs after Noise XX handshake completes,
478/// before sync streams are opened.
479///
480/// When both peers support ML-KEM-768, the upgrade must complete
481/// before any sync stream traffic begins, protecting against
482/// store-now-decrypt-later attacks.
483pub struct KemUpgradeProtocol {
484    /// Local KEM config.
485    pub local: KemConfig,
486    /// Remote KEM config (from WeftHandshake).
487    pub remote: KemConfig,
488    /// Negotiation result.
489    pub result: KemNegotiationResult,
490}
491
492impl KemUpgradeProtocol {
493    /// Create a new KEM upgrade protocol from local and remote configs.
494    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    /// Whether sync streams should wait for KEM to complete.
504    ///
505    /// Returns `true` when both sides support KEM, meaning the
506    /// PQ key exchange MUST finish before any sync traffic flows.
507    pub fn requires_upgrade(&self) -> bool {
508        self.result == KemNegotiationResult::BothSupported
509    }
510
511    /// Execute the KEM exchange, combining the classical Noise secret
512    /// with the post-quantum shared secret (when both sides support it).
513    ///
514    /// In the current placeholder implementation, the PQ secret is
515    /// derived deterministically from the remote KEM public key.
516    /// A real implementation would use ML-KEM-768 encapsulate/decapsulate.
517    pub fn execute(&self, classical_secret: Vec<u8>) -> HybridKeyExchange {
518        match self.result {
519            KemNegotiationResult::BothSupported => {
520                // In real impl: ML-KEM-768 encapsulate/decapsulate.
521                // Placeholder: derive PQ secret from remote public key.
522                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// ── In-memory mock stream for testing ───────────────────────────
539
540#[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    /// Shared buffer backing a pair of mock streams.
551    #[derive(Debug, Default)]
552    pub struct MockBuffer {
553        pub data: VecDeque<Vec<u8>>,
554    }
555
556    /// An in-memory mock [`MeshStream`] for unit tests.
557    pub struct MockStream {
558        /// Outbound buffer (what this side sends).
559        pub tx: Arc<Mutex<MockBuffer>>,
560        /// Inbound buffer (what this side receives).
561        pub rx: Arc<Mutex<MockBuffer>>,
562        pub closed: bool,
563    }
564
565    impl MockStream {
566        /// Create a connected pair of mock streams.
567        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    // ── Key rotation tests ────────────────────────────────────────
712
713    #[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), // immediate
728            std::time::Duration::from_secs(60),
729        );
730        // With zero lifetime, needs rotation immediately
731        assert!(state.needs_rotation());
732        // But old key is still valid within grace period
733        assert!(state.old_key_valid());
734
735        state.begin_rotation();
736        assert!(state.rotating);
737        assert!(state.old_key_valid()); // always valid during rotation
738    }
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), // long grace
745        );
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        // After completion, old key valid within grace period
755        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        // After close, send should fail
778        let result = chan.send_encrypted(b"after close").await;
779        assert!(result.is_err());
780    }
781
782    // ── KEM negotiation and hybrid handshake tests ───────────────
783
784    #[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        // Local supports, remote does not.
803        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        // Remote supports, local does not.
817        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        // Final key should differ from classical-only derivation.
851        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        // Both sides support KEM: requires_upgrade is true and execute
906        // produces a hybrid key with PQ active.
907        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        // Classical-only fallback produces different key.
926        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        // When degraded, execute produces classical-only key.
936        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}