Skip to main content

rns_core/link/
mod.rs

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