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