Skip to main content

fips_core/peer/active/
rekey.rs

1use super::*;
2
3impl ActivePeer {
4    // === Rekey (Key Rotation) ===
5
6    /// When the current Noise session was established.
7    pub fn session_established_at(&self) -> Instant {
8        self.session_established_at
9    }
10
11    #[cfg(test)]
12    pub(crate) fn set_session_established_at_for_test(&mut self, instant: Instant) {
13        self.session_established_at = instant;
14    }
15
16    /// Per-session symmetric rekey-timer jitter offset (seconds).
17    pub fn rekey_jitter_secs(&self) -> i64 {
18        self.rekey_jitter_secs
19    }
20
21    /// Current K-bit epoch value.
22    pub fn current_k_bit(&self) -> bool {
23        self.current_k_bit
24    }
25
26    /// Whether a rekey is currently in progress.
27    pub fn rekey_in_progress(&self) -> bool {
28        self.rekey_in_progress
29    }
30
31    /// Mark that a rekey has been initiated.
32    pub fn set_rekey_in_progress(&mut self) {
33        self.rekey_in_progress = true;
34    }
35
36    /// Check if rekey initiation is dampened (peer recently sent us msg1).
37    pub fn is_rekey_dampened(&self, dampening_secs: u64) -> bool {
38        match self.last_peer_rekey {
39            Some(t) => t.elapsed().as_secs() < dampening_secs,
40            None => false,
41        }
42    }
43
44    /// Record that the peer initiated a rekey (for dampening).
45    pub fn record_peer_rekey(&mut self) {
46        self.last_peer_rekey = Some(Instant::now());
47    }
48
49    /// Get the pending new session's our_index.
50    pub fn pending_our_index(&self) -> Option<SessionIndex> {
51        self.pending_our_index
52    }
53
54    /// Get the pending new session's their_index.
55    pub fn pending_their_index(&self) -> Option<SessionIndex> {
56        self.pending_their_index
57    }
58
59    /// Get the previous session's our_index (during drain).
60    pub fn previous_our_index(&self) -> Option<SessionIndex> {
61        self.previous_our_index
62    }
63
64    /// Get the previous session for decryption fallback.
65    pub fn previous_session(&self) -> Option<&NoiseSession> {
66        self.previous_session.as_ref()
67    }
68
69    /// Get mutable access to the previous session for decryption.
70    pub fn previous_session_mut(&mut self) -> Option<&mut NoiseSession> {
71        self.previous_session.as_mut()
72    }
73
74    /// Get the pending new session (completed rekey, not yet cut over).
75    pub fn pending_new_session(&self) -> Option<&NoiseSession> {
76        self.pending_new_session.as_ref()
77    }
78
79    /// Whether this node should drive the K-bit cutover for the pending FMP rekey.
80    pub fn pending_rekey_initiator(&self) -> bool {
81        self.pending_rekey_initiator
82    }
83
84    /// Whether the locally initiated pending FMP rekey has waited long enough
85    /// to cut over. Responders cut over only after observing the peer's K-bit.
86    pub fn pending_rekey_cutover_due(&self, delay: Duration) -> bool {
87        self.pending_rekey_initiator
88            && self
89                .pending_rekey_completed_at
90                .is_some_and(|completed| completed.elapsed() >= delay)
91    }
92
93    /// Store a completed rekey session and its indices.
94    ///
95    /// Called when the rekey handshake completes. Initiators cut over after a
96    /// short grace period; responders hold the session pending until they
97    /// authenticate the peer's K-bit flip.
98    pub fn set_pending_session(
99        &mut self,
100        session: NoiseSession,
101        our_index: SessionIndex,
102        their_index: SessionIndex,
103        initiated_by_local: bool,
104    ) {
105        self.pending_new_session = Some(session);
106        self.pending_our_index = Some(our_index);
107        self.pending_their_index = Some(their_index);
108        self.pending_rekey_initiator = initiated_by_local;
109        self.pending_rekey_completed_at = Some(Instant::now());
110        self.rekey_in_progress = false;
111        // Clear initiator handshake state (index now lives in pending_our_index)
112        self.rekey_our_index = None;
113        self.rekey_handshake = None;
114        self.rekey_msg1 = None;
115        self.rekey_msg1_next_resend = 0;
116        self.rekey_msg1_resend_count = 0;
117    }
118
119    /// Cut over to the pending new session (initiator side).
120    ///
121    /// Moves current session to previous (for drain), promotes pending to current,
122    /// flips the K-bit. Returns the old our_index that should remain in dispatch
123    /// during the drain window.
124    pub fn cutover_to_new_session(&mut self) -> Option<SessionIndex> {
125        let new_session = self.pending_new_session.take()?;
126        let new_our_index = self.pending_our_index.take();
127        let new_their_index = self.pending_their_index.take();
128
129        // Demote current to previous
130        self.previous_session = self.noise_session.take();
131        self.previous_our_index = self.our_index;
132        self.drain_started = Some(Instant::now());
133
134        // Promote pending to current
135        self.noise_session = Some(new_session);
136        self.our_index = new_our_index;
137        self.their_index = new_their_index;
138        self.pending_rekey_initiator = false;
139        self.pending_rekey_completed_at = None;
140
141        // Flip K-bit and reset timing
142        self.current_k_bit = !self.current_k_bit;
143        self.session_established_at = Instant::now();
144        self.session_start = Instant::now();
145        self.session_generation = self.session_generation.wrapping_add(1).max(1);
146        self.rekey_in_progress = false;
147        self.rekey_msg1_resend_count = 0;
148        self.rekey_jitter_secs = draw_rekey_jitter();
149        self.last_heartbeat_sent = None;
150        self.reset_replay_suppressed();
151
152        self.previous_our_index
153    }
154
155    /// Handle receiving a K-bit flip from the peer (responder side).
156    ///
157    /// Promotes pending_new_session to current, demotes current to previous.
158    /// Returns the old our_index for drain tracking.
159    pub fn handle_peer_kbit_flip(&mut self) -> Option<SessionIndex> {
160        let new_session = self.pending_new_session.take()?;
161        let new_our_index = self.pending_our_index.take();
162        let new_their_index = self.pending_their_index.take();
163
164        // Demote current to previous
165        self.previous_session = self.noise_session.take();
166        self.previous_our_index = self.our_index;
167        self.drain_started = Some(Instant::now());
168
169        // Promote pending to current
170        self.noise_session = Some(new_session);
171        self.our_index = new_our_index;
172        self.their_index = new_their_index;
173        self.pending_rekey_initiator = false;
174        self.pending_rekey_completed_at = None;
175
176        // Match peer's K-bit
177        self.current_k_bit = !self.current_k_bit;
178        self.session_established_at = Instant::now();
179        self.session_start = Instant::now();
180        self.session_generation = self.session_generation.wrapping_add(1).max(1);
181        self.rekey_in_progress = false;
182        self.rekey_msg1_resend_count = 0;
183        self.rekey_jitter_secs = draw_rekey_jitter();
184        self.last_heartbeat_sent = None;
185        self.reset_replay_suppressed();
186
187        self.previous_our_index
188    }
189
190    /// Check if the drain window has expired.
191    pub fn drain_expired(&self, drain_secs: u64) -> bool {
192        match self.drain_started {
193            Some(t) => t.elapsed().as_secs() >= drain_secs,
194            None => false,
195        }
196    }
197
198    /// Whether a drain is in progress.
199    pub fn is_draining(&self) -> bool {
200        self.drain_started.is_some()
201    }
202
203    /// Complete the drain: drop previous session and free its index.
204    ///
205    /// Returns the previous our_index so the caller can remove it from
206    /// the registry and free it from the IndexAllocator.
207    pub fn complete_drain(&mut self) -> Option<SessionIndex> {
208        self.previous_session = None;
209        self.drain_started = None;
210        self.previous_our_index.take()
211    }
212
213    /// Abandon an in-progress rekey.
214    ///
215    /// Returns the rekey our_index so the caller can free it.
216    /// Also clears any pending session state if the handshake was completed
217    /// but not yet cut over.
218    pub fn abandon_rekey(&mut self) -> Option<SessionIndex> {
219        self.rekey_handshake = None;
220        self.rekey_msg1 = None;
221        self.rekey_msg1_next_resend = 0;
222        self.rekey_msg1_resend_count = 0;
223        self.rekey_in_progress = false;
224        // Return whichever index needs freeing
225        self.rekey_our_index.take().or_else(|| {
226            self.pending_new_session = None;
227            self.pending_their_index = None;
228            self.pending_rekey_initiator = false;
229            self.pending_rekey_completed_at = None;
230            self.pending_our_index.take()
231        })
232    }
233
234    // === Rekey Handshake State (Initiator) ===
235
236    /// Store rekey handshake state after sending msg1.
237    pub fn set_rekey_state(
238        &mut self,
239        handshake: NoiseHandshakeState,
240        our_index: SessionIndex,
241        wire_msg1: Vec<u8>,
242        next_resend_ms: u64,
243    ) {
244        self.rekey_handshake = Some(handshake);
245        self.rekey_our_index = Some(our_index);
246        self.rekey_msg1 = Some(wire_msg1);
247        self.rekey_msg1_next_resend = next_resend_ms;
248        self.rekey_msg1_resend_count = 0;
249        self.rekey_in_progress = true;
250    }
251
252    /// Get the rekey our_index (for msg2 dispatch lookup).
253    pub fn rekey_our_index(&self) -> Option<SessionIndex> {
254        self.rekey_our_index
255    }
256
257    /// Complete the rekey by processing msg2 (initiator side).
258    ///
259    /// Takes the stored handshake state, reads msg2, and returns the
260    /// completed NoiseSession. Clears the handshake-related fields but
261    /// leaves rekey_our_index for set_pending_session to use.
262    pub fn complete_rekey_msg2(
263        &mut self,
264        msg2_bytes: &[u8],
265    ) -> Result<(NoiseSession, Option<[u8; 8]>), NoiseError> {
266        let mut hs = self
267            .rekey_handshake
268            .take()
269            .ok_or_else(|| NoiseError::WrongState {
270                expected: "rekey handshake in progress".to_string(),
271                got: "no handshake state".to_string(),
272            })?;
273
274        hs.read_message_2(msg2_bytes)?;
275        let remote_epoch = hs.remote_epoch();
276        let session = hs.into_session()?;
277
278        // Clear msg1 resend state
279        self.rekey_msg1 = None;
280        self.rekey_msg1_next_resend = 0;
281        self.rekey_msg1_resend_count = 0;
282
283        Ok((session, remote_epoch))
284    }
285
286    /// Check if msg1 needs resending.
287    pub fn needs_msg1_resend(&self, now_ms: u64) -> bool {
288        self.rekey_in_progress && self.rekey_msg1.is_some() && now_ms >= self.rekey_msg1_next_resend
289    }
290
291    /// Get msg1 bytes for resend (without consuming).
292    pub fn rekey_msg1(&self) -> Option<&[u8]> {
293        self.rekey_msg1.as_deref()
294    }
295
296    /// Update next resend timestamp.
297    pub fn set_msg1_next_resend(&mut self, next_ms: u64) {
298        self.rekey_msg1_next_resend = next_ms;
299    }
300
301    /// Number of rekey msg1 retransmissions performed so far.
302    pub fn rekey_msg1_resend_count(&self) -> u32 {
303        self.rekey_msg1_resend_count
304    }
305
306    /// Record a rekey msg1 retransmission and schedule the next one.
307    pub fn record_rekey_msg1_resend(&mut self, next_ms: u64) {
308        self.rekey_msg1_resend_count = self.rekey_msg1_resend_count.saturating_add(1);
309        self.rekey_msg1_next_resend = next_ms;
310    }
311}