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