Skip to main content

rns_core/link/
mod.rs

1pub mod crypto;
2pub mod handshake;
3pub mod identify;
4pub mod keepalive;
5pub mod types;
6
7use alloc::vec::Vec;
8
9use rns_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
10use rns_crypto::token::Token;
11use rns_crypto::x25519::X25519PrivateKey;
12use rns_crypto::Rng;
13
14use crate::constants::{
15    LINK_ECPUBSIZE, LINK_ESTABLISHMENT_TIMEOUT_PER_HOP, LINK_KEEPALIVE_MAX, MTU,
16};
17
18pub use types::{LinkAction, LinkError, LinkId, LinkMode, LinkState, TeardownReason};
19
20use crypto::{create_session_token, link_decrypt, link_encrypt};
21use handshake::{
22    build_linkrequest_data, compute_link_id, pack_rtt, parse_linkrequest_data,
23    perform_key_exchange, unpack_rtt, validate_lrproof,
24};
25use keepalive::{
26    compute_establishment_timeout, compute_keepalive, compute_stale_time, is_establishment_timeout,
27    should_go_stale, should_send_keepalive,
28};
29
30/// The Link Engine manages a single link's lifecycle.
31///
32/// It follows the action-queue model: methods return `Vec<LinkAction>` instead
33/// of performing I/O directly. The caller dispatches actions.
34pub struct LinkEngine {
35    link_id: LinkId,
36    state: LinkState,
37    is_initiator: bool,
38    mode: LinkMode,
39
40    // Ephemeral keys
41    prv: X25519PrivateKey,
42
43    // Peer keys
44    peer_pub_bytes: Option<[u8; 32]>,
45    peer_sig_pub_bytes: Option<[u8; 32]>,
46
47    // Session crypto
48    derived_key: Option<Vec<u8>>,
49    token: Option<Token>,
50
51    // Timing
52    request_time: f64,
53    activated_at: Option<f64>,
54    last_inbound: f64,
55    last_outbound: f64,
56    last_keepalive: f64,
57    last_proof: f64,
58    rtt: Option<f64>,
59    keepalive_interval: f64,
60    stale_time: f64,
61    establishment_timeout: f64,
62
63    // Identity
64    remote_identity: Option<([u8; 16], [u8; 64])>,
65    destination_hash: [u8; 16],
66
67    // MDU
68    mtu: u32,
69    mdu: usize,
70}
71
72impl LinkEngine {
73    /// Create a new initiator-side link engine.
74    ///
75    /// Returns `(engine, linkrequest_data)` — the caller must pack linkrequest_data
76    /// into a LINKREQUEST packet and send it.
77    pub fn new_initiator(
78        dest_hash: &[u8; 16],
79        hops: u8,
80        mode: LinkMode,
81        mtu: Option<u32>,
82        now: f64,
83        rng: &mut dyn Rng,
84    ) -> (Self, Vec<u8>) {
85        let prv = X25519PrivateKey::generate(rng);
86        let pub_bytes = prv.public_key().public_bytes();
87        let sig_prv = Ed25519PrivateKey::generate(rng);
88        let sig_pub_bytes = sig_prv.public_key().public_bytes();
89
90        let request_data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, mtu, mode);
91
92        let link_mtu = mtu.unwrap_or(MTU as u32);
93
94        let engine = LinkEngine {
95            link_id: [0u8; 16], // will be set after packet is built
96            state: LinkState::Pending,
97            is_initiator: true,
98            mode,
99            prv,
100            peer_pub_bytes: None,
101            peer_sig_pub_bytes: None,
102            derived_key: None,
103            token: None,
104            request_time: now,
105            activated_at: None,
106            last_inbound: now,
107            last_outbound: now,
108            last_keepalive: now,
109            last_proof: 0.0,
110            rtt: None,
111            keepalive_interval: LINK_KEEPALIVE_MAX,
112            stale_time: LINK_KEEPALIVE_MAX * 2.0,
113            establishment_timeout: compute_establishment_timeout(
114                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
115                hops,
116            ),
117            remote_identity: None,
118            destination_hash: *dest_hash,
119            mtu: link_mtu,
120            mdu: compute_mdu(link_mtu as usize),
121        };
122
123        (engine, request_data)
124    }
125
126    /// Set link_id from the hashable part of the packed LINKREQUEST packet.
127    ///
128    /// Must be called after packing the LINKREQUEST packet (since link_id depends
129    /// on the packet's hashable part).
130    pub fn set_link_id_from_hashable(&mut self, hashable_part: &[u8], data_len: usize) {
131        let extra = data_len.saturating_sub(LINK_ECPUBSIZE);
132        self.link_id = compute_link_id(hashable_part, extra);
133    }
134
135    /// Create a new responder-side link engine from an incoming LINKREQUEST.
136    ///
137    /// `owner_sig_prv` / `owner_sig_pub` are the destination's signing keys.
138    /// Returns `(engine, actions)` where actions include the LRPROOF data to send.
139    #[allow(clippy::too_many_arguments)]
140    pub fn new_responder(
141        owner_sig_prv: &Ed25519PrivateKey,
142        owner_sig_pub_bytes: &[u8; 32],
143        linkrequest_data: &[u8],
144        hashable_part: &[u8],
145        dest_hash: &[u8; 16],
146        hops: u8,
147        now: f64,
148        rng: &mut dyn Rng,
149    ) -> Result<(Self, Vec<u8>), LinkError> {
150        let (peer_pub, peer_sig_pub, peer_mtu, mode) = parse_linkrequest_data(linkrequest_data)?;
151
152        let extra = linkrequest_data.len().saturating_sub(LINK_ECPUBSIZE);
153        let link_id = compute_link_id(hashable_part, extra);
154
155        // Generate ephemeral keys for this end
156        let prv = X25519PrivateKey::generate(rng);
157        let pub_bytes = prv.public_key().public_bytes();
158        let sig_pub_bytes = *owner_sig_pub_bytes;
159
160        // Perform ECDH + HKDF
161        let derived_key = perform_key_exchange(&prv, &peer_pub, &link_id, mode)?;
162        let token = create_session_token(&derived_key)?;
163
164        let link_mtu = peer_mtu.unwrap_or(MTU as u32);
165
166        // Build LRPROOF
167        let lrproof_data = handshake::build_lrproof(
168            &link_id,
169            &pub_bytes,
170            &sig_pub_bytes,
171            owner_sig_prv,
172            peer_mtu,
173            mode,
174        );
175
176        let engine = LinkEngine {
177            link_id,
178            state: LinkState::Handshake,
179            is_initiator: false,
180            mode,
181            prv,
182            peer_pub_bytes: Some(peer_pub),
183            peer_sig_pub_bytes: Some(peer_sig_pub),
184            derived_key: Some(derived_key),
185            token: Some(token),
186            request_time: now,
187            activated_at: None,
188            last_inbound: now,
189            last_outbound: now,
190            last_keepalive: now,
191            last_proof: 0.0,
192            rtt: None,
193            keepalive_interval: LINK_KEEPALIVE_MAX,
194            stale_time: LINK_KEEPALIVE_MAX * 2.0,
195            establishment_timeout: compute_establishment_timeout(
196                LINK_ESTABLISHMENT_TIMEOUT_PER_HOP,
197                hops,
198            ),
199            remote_identity: None,
200            destination_hash: *dest_hash,
201            mtu: link_mtu,
202            mdu: compute_mdu(link_mtu as usize),
203        };
204
205        Ok((engine, lrproof_data))
206    }
207
208    /// Handle an incoming LRPROOF (initiator side).
209    ///
210    /// Validates the proof, performs ECDH, derives session key, returns LRRTT data
211    /// to be encrypted and sent.
212    pub fn handle_lrproof(
213        &mut self,
214        proof_data: &[u8],
215        peer_sig_pub_bytes: &[u8; 32],
216        now: f64,
217        rng: &mut dyn Rng,
218    ) -> Result<(Vec<u8>, Vec<LinkAction>), LinkError> {
219        if self.state != LinkState::Pending || !self.is_initiator {
220            return Err(LinkError::InvalidState);
221        }
222
223        let peer_sig_pub = Ed25519PublicKey::from_bytes(peer_sig_pub_bytes);
224
225        let (peer_pub, confirmed_mtu, confirmed_mode) =
226            validate_lrproof(proof_data, &self.link_id, &peer_sig_pub, peer_sig_pub_bytes)?;
227
228        if confirmed_mode != self.mode {
229            return Err(LinkError::UnsupportedMode);
230        }
231
232        self.peer_pub_bytes = Some(peer_pub);
233        self.peer_sig_pub_bytes = Some(*peer_sig_pub_bytes);
234
235        // ECDH + HKDF
236        let derived_key = perform_key_exchange(&self.prv, &peer_pub, &self.link_id, self.mode)?;
237        let token = create_session_token(&derived_key)?;
238
239        self.derived_key = Some(derived_key);
240        self.token = Some(token);
241
242        // Update MTU if confirmed
243        if let Some(mtu) = confirmed_mtu {
244            self.mtu = mtu;
245            self.mdu = compute_mdu(mtu as usize);
246        }
247
248        // Compute RTT and activate
249        let rtt = now - self.request_time;
250        self.rtt = Some(rtt);
251        self.state = LinkState::Active;
252        self.activated_at = Some(now);
253        self.last_inbound = now;
254        self.update_keepalive();
255
256        // Build encrypted LRRTT packet data
257        let rtt_packed = pack_rtt(rtt);
258        let rtt_encrypted = self.encrypt(&rtt_packed, rng)?;
259
260        let actions = vec![
261            LinkAction::StateChanged {
262                link_id: self.link_id,
263                new_state: LinkState::Active,
264                reason: None,
265            },
266            LinkAction::LinkEstablished {
267                link_id: self.link_id,
268                rtt,
269                is_initiator: true,
270            },
271        ];
272
273        Ok((rtt_encrypted, actions))
274    }
275
276    /// Handle an incoming LRRTT (responder side).
277    ///
278    /// Decrypts the RTT packet, activates the link.
279    pub fn handle_lrrtt(
280        &mut self,
281        encrypted_data: &[u8],
282        now: f64,
283    ) -> Result<Vec<LinkAction>, LinkError> {
284        if self.state != LinkState::Handshake || self.is_initiator {
285            return Err(LinkError::InvalidState);
286        }
287
288        let plaintext = self.decrypt(encrypted_data)?;
289        let initiator_rtt = unpack_rtt(&plaintext).ok_or(LinkError::InvalidData)?;
290
291        let measured_rtt = now - self.request_time;
292        let rtt = if measured_rtt > initiator_rtt {
293            measured_rtt
294        } else {
295            initiator_rtt
296        };
297
298        self.rtt = Some(rtt);
299        self.state = LinkState::Active;
300        self.activated_at = Some(now);
301        self.last_inbound = now;
302        self.update_keepalive();
303
304        let actions = vec![
305            LinkAction::StateChanged {
306                link_id: self.link_id,
307                new_state: LinkState::Active,
308                reason: None,
309            },
310            LinkAction::LinkEstablished {
311                link_id: self.link_id,
312                rtt,
313                is_initiator: false,
314            },
315        ];
316
317        Ok(actions)
318    }
319
320    /// Encrypt plaintext for transmission over this link.
321    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Result<Vec<u8>, LinkError> {
322        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
323        Ok(link_encrypt(token, plaintext, rng))
324    }
325
326    /// Decrypt ciphertext received on this link.
327    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, LinkError> {
328        let token = self.token.as_ref().ok_or(LinkError::NoSessionKey)?;
329        link_decrypt(token, ciphertext)
330    }
331
332    /// Build LINKIDENTIFY data (encrypted).
333    pub fn build_identify(
334        &self,
335        identity: &rns_crypto::identity::Identity,
336        rng: &mut dyn Rng,
337    ) -> Result<Vec<u8>, LinkError> {
338        if self.state != LinkState::Active {
339            return Err(LinkError::InvalidState);
340        }
341        let plaintext = identify::build_identify_data(identity, &self.link_id)?;
342        self.encrypt(&plaintext, rng)
343    }
344
345    /// Handle incoming LINKIDENTIFY (encrypted data).
346    ///
347    /// Only responders (non-initiators) can receive LINKIDENTIFY (Python: Link.py:1017).
348    pub fn handle_identify(&mut self, encrypted_data: &[u8]) -> Result<Vec<LinkAction>, LinkError> {
349        if self.state != LinkState::Active || self.is_initiator {
350            return Err(LinkError::InvalidState);
351        }
352
353        let plaintext = self.decrypt(encrypted_data)?;
354        let (identity_hash, public_key) =
355            identify::validate_identify_data(&plaintext, &self.link_id)?;
356        self.remote_identity = Some((identity_hash, public_key));
357
358        Ok(alloc::vec![LinkAction::RemoteIdentified {
359            link_id: self.link_id,
360            identity_hash,
361            public_key,
362        }])
363    }
364
365    /// Record that an inbound packet was received (updates timing).
366    ///
367    /// If the link is STALE, recovers to ACTIVE (Python: Link.py:987-988).
368    pub fn record_inbound(&mut self, now: f64) -> Vec<LinkAction> {
369        self.last_inbound = now;
370        if self.state == LinkState::Stale {
371            self.state = LinkState::Active;
372            return alloc::vec![LinkAction::StateChanged {
373                link_id: self.link_id,
374                new_state: LinkState::Active,
375                reason: None,
376            }];
377        }
378        Vec::new()
379    }
380
381    /// Record that a proof was received (updates timing for stale detection).
382    pub fn record_proof(&mut self, now: f64) {
383        self.last_proof = now;
384    }
385
386    /// Record that an outbound packet was sent (updates timing).
387    pub fn record_outbound(&mut self, now: f64, is_keepalive: bool) {
388        self.last_outbound = now;
389        if is_keepalive {
390            self.last_keepalive = now;
391        }
392    }
393
394    /// Periodic tick: check keepalive, stale, timeouts.
395    pub fn tick(&mut self, now: f64) -> Vec<LinkAction> {
396        let mut actions = Vec::new();
397
398        match self.state {
399            LinkState::Pending | LinkState::Handshake => {
400                if is_establishment_timeout(self.request_time, self.establishment_timeout, now) {
401                    self.state = LinkState::Closed;
402                    actions.push(LinkAction::StateChanged {
403                        link_id: self.link_id,
404                        new_state: LinkState::Closed,
405                        reason: Some(TeardownReason::Timeout),
406                    });
407                }
408            }
409            LinkState::Active => {
410                let activated = self.activated_at.unwrap_or(0.0);
411                // Python: max(max(self.last_inbound, self.last_proof), activated_at)
412                let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
413
414                if should_go_stale(last_inbound, self.stale_time, now) {
415                    self.state = LinkState::Stale;
416                    actions.push(LinkAction::StateChanged {
417                        link_id: self.link_id,
418                        new_state: LinkState::Stale,
419                        reason: None,
420                    });
421                }
422            }
423            LinkState::Stale => {
424                // In Python, STALE immediately sends teardown and closes
425                self.state = LinkState::Closed;
426                actions.push(LinkAction::StateChanged {
427                    link_id: self.link_id,
428                    new_state: LinkState::Closed,
429                    reason: Some(TeardownReason::Timeout),
430                });
431            }
432            LinkState::Closed => {}
433        }
434
435        actions
436    }
437
438    /// Check if a keepalive should be sent. Returns true if conditions are met.
439    pub fn needs_keepalive(&self, now: f64) -> bool {
440        if self.state != LinkState::Active {
441            return false;
442        }
443        let activated = self.activated_at.unwrap_or(0.0);
444        let last_inbound = self.last_inbound.max(self.last_proof).max(activated);
445
446        // Only send keepalive when past keepalive interval from last inbound
447        if now < last_inbound + self.keepalive_interval {
448            return false;
449        }
450
451        should_send_keepalive(self.last_keepalive, self.keepalive_interval, now)
452    }
453
454    /// Tear down the link (initiator-initiated close).
455    pub fn teardown(&mut self) -> Vec<LinkAction> {
456        if self.state == LinkState::Closed {
457            return Vec::new();
458        }
459        self.state = LinkState::Closed;
460        let reason = if self.is_initiator {
461            TeardownReason::InitiatorClosed
462        } else {
463            TeardownReason::DestinationClosed
464        };
465        alloc::vec![LinkAction::StateChanged {
466            link_id: self.link_id,
467            new_state: LinkState::Closed,
468            reason: Some(reason),
469        }]
470    }
471
472    /// Handle incoming teardown (remote close).
473    pub fn handle_teardown(&mut self) -> Vec<LinkAction> {
474        if self.state == LinkState::Closed {
475            return Vec::new();
476        }
477        self.state = LinkState::Closed;
478        let reason = if self.is_initiator {
479            TeardownReason::DestinationClosed
480        } else {
481            TeardownReason::InitiatorClosed
482        };
483        alloc::vec![LinkAction::StateChanged {
484            link_id: self.link_id,
485            new_state: LinkState::Closed,
486            reason: Some(reason),
487        }]
488    }
489
490    // --- Queries ---
491
492    pub fn link_id(&self) -> &LinkId {
493        &self.link_id
494    }
495
496    pub fn state(&self) -> LinkState {
497        self.state
498    }
499
500    pub fn rtt(&self) -> Option<f64> {
501        self.rtt
502    }
503
504    pub fn mdu(&self) -> usize {
505        self.mdu
506    }
507
508    pub fn mtu(&self) -> u32 {
509        self.mtu
510    }
511
512    pub fn is_initiator(&self) -> bool {
513        self.is_initiator
514    }
515
516    pub fn mode(&self) -> LinkMode {
517        self.mode
518    }
519
520    pub fn remote_identity(&self) -> Option<&([u8; 16], [u8; 64])> {
521        self.remote_identity.as_ref()
522    }
523
524    pub fn destination_hash(&self) -> &[u8; 16] {
525        &self.destination_hash
526    }
527
528    /// Get the derived session key (needed for hole-punch token derivation).
529    pub fn derived_key(&self) -> Option<&[u8]> {
530        self.derived_key.as_deref()
531    }
532
533    pub fn keepalive_interval(&self) -> f64 {
534        self.keepalive_interval
535    }
536
537    /// Update the measured RTT (e.g., after path redirect to a direct link).
538    /// Also recalculates keepalive and stale timers.
539    pub fn set_rtt(&mut self, rtt: f64) {
540        self.rtt = Some(rtt);
541        self.update_keepalive();
542    }
543
544    /// Update the link MTU (e.g., after path redirect to a different interface).
545    pub fn set_mtu(&mut self, mtu: u32) {
546        self.mtu = mtu;
547        self.mdu = compute_mdu(mtu as usize);
548    }
549
550    #[doc(hidden)]
551    pub fn clear_session_for_testing(&mut self) {
552        self.derived_key = None;
553        self.token = None;
554    }
555
556    // --- Internal ---
557
558    fn update_keepalive(&mut self) {
559        if let Some(rtt) = self.rtt {
560            self.keepalive_interval = compute_keepalive(rtt);
561            self.stale_time = compute_stale_time(self.keepalive_interval);
562        }
563    }
564}
565
566/// Compute link MDU from MTU.
567///
568/// MDU = floor((mtu - IFAC_MIN_SIZE - HEADER_MINSIZE - TOKEN_OVERHEAD) / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
569fn compute_mdu(mtu: usize) -> usize {
570    use crate::constants::{AES128_BLOCKSIZE, HEADER_MINSIZE, IFAC_MIN_SIZE, TOKEN_OVERHEAD};
571    let numerator = mtu.saturating_sub(IFAC_MIN_SIZE + HEADER_MINSIZE + TOKEN_OVERHEAD);
572    (numerator / AES128_BLOCKSIZE) * AES128_BLOCKSIZE - 1
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::constants::LINK_MDU;
579    use rns_crypto::FixedRng;
580
581    fn make_rng(seed: u8) -> FixedRng {
582        FixedRng::new(&[seed; 128])
583    }
584
585    #[test]
586    fn test_compute_mdu_default() {
587        assert_eq!(compute_mdu(500), LINK_MDU);
588    }
589
590    #[test]
591    fn test_full_handshake() {
592        // Setup: destination identity (for responder)
593        let mut rng_id = make_rng(0x01);
594        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
595        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
596
597        let dest_hash = [0xDD; 16];
598        let mode = LinkMode::Aes256Cbc;
599
600        // Step 1: Initiator creates link request
601        let mut rng_init = make_rng(0x10);
602        let (mut initiator, request_data) =
603            LinkEngine::new_initiator(&dest_hash, 1, mode, Some(500), 100.0, &mut rng_init);
604        assert_eq!(initiator.state(), LinkState::Pending);
605
606        // Simulate packet packing: build a fake hashable part
607        // In real usage, the caller packs a LINKREQUEST packet and calls set_link_id_from_hashable
608        let mut hashable = Vec::new();
609        hashable.push(0x00); // flags byte (lower nibble)
610        hashable.push(0x00); // hops
611        hashable.extend_from_slice(&dest_hash);
612        hashable.push(0x00); // context
613        hashable.extend_from_slice(&request_data);
614
615        initiator.set_link_id_from_hashable(&hashable, request_data.len());
616        assert_ne!(initiator.link_id(), &[0u8; 16]);
617
618        // Step 2: Responder receives link request
619        let mut rng_resp = make_rng(0x20);
620        let (mut responder, lrproof_data) = LinkEngine::new_responder(
621            &dest_sig_prv,
622            &dest_sig_pub_bytes,
623            &request_data,
624            &hashable,
625            &dest_hash,
626            1,
627            100.5,
628            &mut rng_resp,
629        )
630        .unwrap();
631        assert_eq!(responder.state(), LinkState::Handshake);
632        assert_eq!(responder.link_id(), initiator.link_id());
633
634        // Step 3: Initiator validates LRPROOF
635        let mut rng_lrrtt = make_rng(0x30);
636        let (lrrtt_encrypted, actions) = initiator
637            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
638            .unwrap();
639        assert_eq!(initiator.state(), LinkState::Active);
640        assert!(initiator.rtt().is_some());
641        assert_eq!(actions.len(), 2); // StateChanged + LinkEstablished
642
643        // Step 4: Responder handles LRRTT
644        let actions = responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
645        assert_eq!(responder.state(), LinkState::Active);
646        assert!(responder.rtt().is_some());
647        assert_eq!(actions.len(), 2);
648    }
649
650    #[test]
651    fn test_encrypt_decrypt_after_handshake() {
652        let mut rng_id = make_rng(0x01);
653        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
654        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
655        let dest_hash = [0xDD; 16];
656
657        let mut rng_init = make_rng(0x10);
658        let (mut initiator, request_data) = LinkEngine::new_initiator(
659            &dest_hash,
660            1,
661            LinkMode::Aes256Cbc,
662            Some(500),
663            100.0,
664            &mut rng_init,
665        );
666        let mut hashable = Vec::new();
667        hashable.push(0x00);
668        hashable.push(0x00);
669        hashable.extend_from_slice(&dest_hash);
670        hashable.push(0x00);
671        hashable.extend_from_slice(&request_data);
672        initiator.set_link_id_from_hashable(&hashable, request_data.len());
673
674        let mut rng_resp = make_rng(0x20);
675        let (mut responder, lrproof_data) = LinkEngine::new_responder(
676            &dest_sig_prv,
677            &dest_sig_pub_bytes,
678            &request_data,
679            &hashable,
680            &dest_hash,
681            1,
682            100.5,
683            &mut rng_resp,
684        )
685        .unwrap();
686
687        let mut rng_lrrtt = make_rng(0x30);
688        let (lrrtt_encrypted, _) = initiator
689            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
690            .unwrap();
691        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
692
693        // Now both sides are ACTIVE — test encrypt/decrypt
694        let mut rng_enc = make_rng(0x40);
695        let plaintext = b"Hello over encrypted link!";
696        let ciphertext = initiator.encrypt(plaintext, &mut rng_enc).unwrap();
697        let decrypted = responder.decrypt(&ciphertext).unwrap();
698        assert_eq!(decrypted, plaintext);
699
700        // And in reverse
701        let mut rng_enc2 = make_rng(0x50);
702        let ciphertext2 = responder.encrypt(b"Reply!", &mut rng_enc2).unwrap();
703        let decrypted2 = initiator.decrypt(&ciphertext2).unwrap();
704        assert_eq!(decrypted2, b"Reply!");
705    }
706
707    #[test]
708    fn test_tick_establishment_timeout() {
709        let mut rng = make_rng(0x10);
710        let dest_hash = [0xDD; 16];
711        let (mut engine, _) =
712            LinkEngine::new_initiator(&dest_hash, 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
713        // Timeout = 6.0 + 6.0 * 1 = 12.0s → expires at 112.0
714
715        // Before timeout — no state change
716        let actions = engine.tick(110.0);
717        assert!(actions.is_empty());
718
719        // After timeout
720        let actions = engine.tick(113.0);
721        assert_eq!(actions.len(), 1);
722        assert_eq!(engine.state(), LinkState::Closed);
723    }
724
725    #[test]
726    fn test_tick_stale_and_close() {
727        let mut rng_id = make_rng(0x01);
728        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
729        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
730        let dest_hash = [0xDD; 16];
731
732        let mut rng_init = make_rng(0x10);
733        let (mut initiator, request_data) = LinkEngine::new_initiator(
734            &dest_hash,
735            1,
736            LinkMode::Aes256Cbc,
737            Some(500),
738            100.0,
739            &mut rng_init,
740        );
741        let mut hashable = Vec::new();
742        hashable.push(0x00);
743        hashable.push(0x00);
744        hashable.extend_from_slice(&dest_hash);
745        hashable.push(0x00);
746        hashable.extend_from_slice(&request_data);
747        initiator.set_link_id_from_hashable(&hashable, request_data.len());
748
749        let mut rng_resp = make_rng(0x20);
750        let (_, lrproof_data) = LinkEngine::new_responder(
751            &dest_sig_prv,
752            &dest_sig_pub_bytes,
753            &request_data,
754            &hashable,
755            &dest_hash,
756            1,
757            100.5,
758            &mut rng_resp,
759        )
760        .unwrap();
761
762        let mut rng_lrrtt = make_rng(0x30);
763        initiator
764            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
765            .unwrap();
766        assert_eq!(initiator.state(), LinkState::Active);
767
768        // Advance time past stale_time
769        let stale_time = initiator.stale_time;
770        let actions = initiator.tick(100.8 + stale_time + 1.0);
771        assert_eq!(initiator.state(), LinkState::Stale);
772        assert_eq!(actions.len(), 1);
773
774        // Next tick: STALE → CLOSED
775        let actions = initiator.tick(100.8 + stale_time + 2.0);
776        assert_eq!(initiator.state(), LinkState::Closed);
777        assert_eq!(actions.len(), 1);
778    }
779
780    #[test]
781    fn test_needs_keepalive() {
782        let mut rng_id = make_rng(0x01);
783        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
784        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
785        let dest_hash = [0xDD; 16];
786
787        let mut rng_init = make_rng(0x10);
788        let (mut initiator, request_data) = LinkEngine::new_initiator(
789            &dest_hash,
790            1,
791            LinkMode::Aes256Cbc,
792            Some(500),
793            100.0,
794            &mut rng_init,
795        );
796        let mut hashable = Vec::new();
797        hashable.push(0x00);
798        hashable.push(0x00);
799        hashable.extend_from_slice(&dest_hash);
800        hashable.push(0x00);
801        hashable.extend_from_slice(&request_data);
802        initiator.set_link_id_from_hashable(&hashable, request_data.len());
803
804        let mut rng_resp = make_rng(0x20);
805        let (_, lrproof_data) = LinkEngine::new_responder(
806            &dest_sig_prv,
807            &dest_sig_pub_bytes,
808            &request_data,
809            &hashable,
810            &dest_hash,
811            1,
812            100.5,
813            &mut rng_resp,
814        )
815        .unwrap();
816
817        let mut rng_lrrtt = make_rng(0x30);
818        initiator
819            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
820            .unwrap();
821
822        let ka = initiator.keepalive_interval();
823        // Not yet
824        assert!(!initiator.needs_keepalive(100.8 + ka - 1.0));
825        // Past keepalive
826        assert!(initiator.needs_keepalive(100.8 + ka + 1.0));
827    }
828
829    #[test]
830    fn test_needs_keepalive_responder() {
831        let mut rng_id = make_rng(0x01);
832        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
833        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
834        let dest_hash = [0xDD; 16];
835
836        let mut rng_init = make_rng(0x10);
837        let (mut initiator, request_data) = LinkEngine::new_initiator(
838            &dest_hash,
839            1,
840            LinkMode::Aes256Cbc,
841            Some(500),
842            100.0,
843            &mut rng_init,
844        );
845        let mut hashable = Vec::new();
846        hashable.push(0x00);
847        hashable.push(0x00);
848        hashable.extend_from_slice(&dest_hash);
849        hashable.push(0x00);
850        hashable.extend_from_slice(&request_data);
851        initiator.set_link_id_from_hashable(&hashable, request_data.len());
852
853        let mut rng_resp = make_rng(0x20);
854        let (mut responder, lrproof_data) = LinkEngine::new_responder(
855            &dest_sig_prv,
856            &dest_sig_pub_bytes,
857            &request_data,
858            &hashable,
859            &dest_hash,
860            1,
861            100.5,
862            &mut rng_resp,
863        )
864        .unwrap();
865
866        let mut rng_lrrtt = make_rng(0x30);
867        let (lrrtt_encrypted, _) = initiator
868            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
869            .unwrap();
870        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
871
872        let ka = responder.keepalive_interval();
873        // Responder should also send keepalives
874        assert!(!responder.needs_keepalive(101.0 + ka - 1.0));
875        assert!(responder.needs_keepalive(101.0 + ka + 1.0));
876    }
877
878    #[test]
879    fn test_teardown() {
880        let mut rng = make_rng(0x10);
881        let (mut engine, _) =
882            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
883        let actions = engine.teardown();
884        assert_eq!(engine.state(), LinkState::Closed);
885        assert_eq!(actions.len(), 1);
886
887        // Teardown again is no-op
888        let actions = engine.teardown();
889        assert!(actions.is_empty());
890    }
891
892    #[test]
893    fn test_handle_teardown() {
894        let mut rng = make_rng(0x10);
895        let (mut engine, _) =
896            LinkEngine::new_initiator(&[0xDD; 16], 1, LinkMode::Aes256Cbc, None, 100.0, &mut rng);
897        let actions = engine.handle_teardown();
898        assert_eq!(engine.state(), LinkState::Closed);
899        assert_eq!(actions.len(), 1);
900        match &actions[0] {
901            LinkAction::StateChanged { reason, .. } => {
902                assert_eq!(*reason, Some(TeardownReason::DestinationClosed));
903            }
904            _ => panic!("Expected StateChanged"),
905        }
906    }
907
908    #[test]
909    fn test_identify_over_link() {
910        let mut rng_id = make_rng(0x01);
911        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
912        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
913        let dest_hash = [0xDD; 16];
914
915        let mut rng_init = make_rng(0x10);
916        let (mut initiator, request_data) = LinkEngine::new_initiator(
917            &dest_hash,
918            1,
919            LinkMode::Aes256Cbc,
920            Some(500),
921            100.0,
922            &mut rng_init,
923        );
924        let mut hashable = Vec::new();
925        hashable.push(0x00);
926        hashable.push(0x00);
927        hashable.extend_from_slice(&dest_hash);
928        hashable.push(0x00);
929        hashable.extend_from_slice(&request_data);
930        initiator.set_link_id_from_hashable(&hashable, request_data.len());
931
932        let mut rng_resp = make_rng(0x20);
933        let (mut responder, lrproof_data) = LinkEngine::new_responder(
934            &dest_sig_prv,
935            &dest_sig_pub_bytes,
936            &request_data,
937            &hashable,
938            &dest_hash,
939            1,
940            100.5,
941            &mut rng_resp,
942        )
943        .unwrap();
944
945        let mut rng_lrrtt = make_rng(0x30);
946        let (lrrtt_encrypted, _) = initiator
947            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
948            .unwrap();
949        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
950
951        // Create identity to identify with
952        let mut rng_ident = make_rng(0x40);
953        let my_identity = rns_crypto::identity::Identity::new(&mut rng_ident);
954
955        // Initiator identifies itself to responder
956        let mut rng_enc = make_rng(0x50);
957        let identify_encrypted = initiator
958            .build_identify(&my_identity, &mut rng_enc)
959            .unwrap();
960
961        let actions = responder.handle_identify(&identify_encrypted).unwrap();
962        assert_eq!(actions.len(), 1);
963        match &actions[0] {
964            LinkAction::RemoteIdentified {
965                identity_hash,
966                public_key,
967                ..
968            } => {
969                assert_eq!(identity_hash, my_identity.hash());
970                assert_eq!(public_key, &my_identity.get_public_key().unwrap());
971            }
972            _ => panic!("Expected RemoteIdentified"),
973        }
974    }
975
976    #[test]
977    fn test_aes128_mode_handshake() {
978        let mut rng_id = make_rng(0x01);
979        let dest_sig_prv = Ed25519PrivateKey::generate(&mut rng_id);
980        let dest_sig_pub_bytes = dest_sig_prv.public_key().public_bytes();
981        let dest_hash = [0xDD; 16];
982
983        let mut rng_init = make_rng(0x10);
984        let (mut initiator, request_data) = LinkEngine::new_initiator(
985            &dest_hash,
986            1,
987            LinkMode::Aes128Cbc,
988            Some(500),
989            100.0,
990            &mut rng_init,
991        );
992        let mut hashable = Vec::new();
993        hashable.push(0x00);
994        hashable.push(0x00);
995        hashable.extend_from_slice(&dest_hash);
996        hashable.push(0x00);
997        hashable.extend_from_slice(&request_data);
998        initiator.set_link_id_from_hashable(&hashable, request_data.len());
999
1000        let mut rng_resp = make_rng(0x20);
1001        let (mut responder, lrproof_data) = LinkEngine::new_responder(
1002            &dest_sig_prv,
1003            &dest_sig_pub_bytes,
1004            &request_data,
1005            &hashable,
1006            &dest_hash,
1007            1,
1008            100.5,
1009            &mut rng_resp,
1010        )
1011        .unwrap();
1012
1013        let mut rng_lrrtt = make_rng(0x30);
1014        let (lrrtt_encrypted, _) = initiator
1015            .handle_lrproof(&lrproof_data, &dest_sig_pub_bytes, 100.8, &mut rng_lrrtt)
1016            .unwrap();
1017        responder.handle_lrrtt(&lrrtt_encrypted, 101.0).unwrap();
1018
1019        assert_eq!(initiator.state(), LinkState::Active);
1020        assert_eq!(responder.state(), LinkState::Active);
1021        assert_eq!(initiator.mode(), LinkMode::Aes128Cbc);
1022
1023        // Verify encrypt/decrypt works
1024        let mut rng_enc = make_rng(0x40);
1025        let ct = initiator.encrypt(b"AES128 test", &mut rng_enc).unwrap();
1026        let pt = responder.decrypt(&ct).unwrap();
1027        assert_eq!(pt, b"AES128 test");
1028    }
1029}