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