Skip to main content

srt_runtime/
km_refresh.rs

1//! SEK-rotation ("KM Refresh") driver — `draft-sharabayko-srt-01` §6.1.6 (KM
2//! Refresh), curated at `specs/rules/srt-crypto.md` ("KM Refresh — §6.1.6").
3//!
4//! Sans-IO: [`KmRefreshDriver::on_packet_sent`] / [`KmRefreshDriver::tick`]
5//! take a caller-supplied packet count — this crate never reads a wall clock
6//! or a socket (the same contract as [`crate::handshake_sm`] / [`crate::arq`]
7//! / [`crate::tsbpd`]). The driver only tracks *when* to rotate and *which*
8//! parity is active; it does not generate, wrap, or send key material
9//! itself — the actual SEK PRNG/wrap/Key-Material-message send is the
10//! caller's job (mirroring [`crate::handshake_sm::CryptoConfig`]'s design:
11//! this crate's sans-IO core never owns a CSPRNG), triggered by
12//! [`KmRefreshEvent::PreAnnounce`].
13//!
14//! # Thresholds (§6.1.6, "Recommended values")
15//!
16//! - **KM Refresh Period = `2^25` packets**: how long a key stays active
17//!   before switchover.
18//! - **KM Pre-Announcement Period = `4000` packets**: how long *before*
19//!   switchover the new key is announced, and — symmetrically — how long
20//!   *after* switchover the old key stays valid before decommission. "Both
21//!   keys are valid in parallel for `2 * Pre-Announcement Period`", to
22//!   tolerate late/retransmitted packets that were encrypted under the old
23//!   key.
24//!
25//! All three thresholds are measured from the start of the *current* active
26//! key's epoch (packet 0 of that key):
27//!
28//! ```text
29//! 0 ── refresh - pre_announce ── refresh ── refresh + pre_announce
30//!      PreAnnounce fires         Switchover  Decommission fires
31//!      (generate + wrap +       (old key     (old key dropped)
32//!       ready next key)          still valid)
33//! ```
34//!
35//! [`KmRefreshThresholds::RECOMMENDED`] is the spec's `2^25`/`4000` pair;
36//! `tests/km_refresh.rs` drives the same state machine with a scaled-down
37//! threshold pair so the test suite does not need `2^25` real iterations —
38//! the state machine logic is identical, only the threshold constants
39//! differ.
40
41use alloc::vec::Vec;
42
43/// Which of the two alternating SEKs (`draft-sharabayko-srt-01` §3.1's data
44/// packet `KK` field / §6.1.6's odd/even alternation) is meant.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47pub enum KeyParity {
48    /// The even-numbered SEK.
49    Even,
50    /// The odd-numbered SEK.
51    Odd,
52}
53
54impl KeyParity {
55    /// The other parity — every rotation alternates (§6.1.6).
56    pub fn other(self) -> Self {
57        match self {
58            KeyParity::Even => KeyParity::Odd,
59            KeyParity::Odd => KeyParity::Even,
60        }
61    }
62
63    /// Spec label.
64    pub fn name(&self) -> &'static str {
65        match self {
66            KeyParity::Even => "even",
67            KeyParity::Odd => "odd",
68        }
69    }
70}
71
72broadcast_common::impl_spec_display!(KeyParity);
73
74/// KM Refresh packet-count thresholds (`draft-sharabayko-srt-01` §6.1.6).
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct KmRefreshThresholds {
77    /// Packets an active key is used for before switchover (spec-recommended
78    /// `2^25`).
79    pub refresh_period: u64,
80    /// Packets before switchover the next key is announced, and after
81    /// switchover the old key stays valid (spec-recommended `4000`).
82    pub pre_announcement_period: u64,
83}
84
85impl KmRefreshThresholds {
86    /// The draft's own recommended values (§6.1.6): `2^25` packets refresh
87    /// period, `4000` packets pre-announcement period.
88    pub const RECOMMENDED: KmRefreshThresholds = KmRefreshThresholds {
89        refresh_period: 1 << 25,
90        pre_announcement_period: 4000,
91    };
92}
93
94/// One state transition [`KmRefreshDriver::on_packet_sent`] /
95/// [`KmRefreshDriver::tick`] can fire (`draft-sharabayko-srt-01` §6.1.6).
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum KmRefreshEvent {
99    /// `refresh_period - pre_announcement_period` packets sent under the
100    /// active key: generate, wrap, and send a fresh SEK for `next_parity`
101    /// now (§6.1.5's Key Material exchange, driven out-of-band of this
102    /// driver — see the module doc).
103    PreAnnounce {
104        /// The parity the newly-generated key will use.
105        next_parity: KeyParity,
106    },
107    /// `refresh_period` packets sent under the active key: start encrypting
108    /// *new* outgoing packets with `new_active` from now on. The previous
109    /// key remains valid for decrypting late/retransmitted packets until
110    /// [`KmRefreshEvent::Decommission`] fires for it.
111    Switchover {
112        /// The parity now active for new packets.
113        new_active: KeyParity,
114    },
115    /// `pre_announcement_period` packets after switchover: the previous key
116    /// may be dropped — no more retransmits will need it (§6.1.6, "both keys
117    /// valid in parallel for `2 * Pre-Announcement Period`").
118    Decommission {
119        /// The parity being retired.
120        retired: KeyParity,
121    },
122}
123
124/// Sans-IO SEK-rotation state machine (`draft-sharabayko-srt-01` §6.1.6).
125///
126/// Tracks which [`KeyParity`] is currently active and fires
127/// [`KmRefreshEvent`]s as the packet count crosses the configured
128/// [`KmRefreshThresholds`]. Does not hold key material itself — see the
129/// module doc. Rotates indefinitely: once a cycle's [`KmRefreshEvent::Decommission`]
130/// fires, the next cycle's thresholds are armed again against the new active
131/// key's epoch.
132#[derive(Debug, Clone, PartialEq)]
133pub struct KmRefreshDriver {
134    thresholds: KmRefreshThresholds,
135    active: KeyParity,
136    /// Packet count at which `active` most recently became active (`0` for
137    /// the initial key negotiated at handshake time).
138    epoch_start: u64,
139    /// Total packets sent so far (monotonic).
140    total_sent: u64,
141    pre_announced: bool,
142    switched_over: bool,
143    decommissioned: bool,
144}
145
146impl KmRefreshDriver {
147    /// A fresh driver. `initial_parity` is the SEK negotiated at handshake
148    /// time (this crate's handshake convention is [`KeyParity::Even`] — see
149    /// `crate::handshake_sm::build_key_material_extension`, `crypto` feature
150    /// only).
151    pub fn new(thresholds: KmRefreshThresholds, initial_parity: KeyParity) -> Self {
152        KmRefreshDriver {
153            thresholds,
154            active: initial_parity,
155            epoch_start: 0,
156            total_sent: 0,
157            pre_announced: false,
158            switched_over: false,
159            decommissioned: false,
160        }
161    }
162
163    /// The currently-active parity for *new* outgoing packets.
164    pub fn active_parity(&self) -> KeyParity {
165        self.active
166    }
167
168    /// Total packets recorded via [`Self::on_packet_sent`]/[`Self::tick`].
169    pub fn total_sent(&self) -> u64 {
170        self.total_sent
171    }
172
173    /// Whether `parity`'s key should still be considered held/valid. The
174    /// active key always is; the just-retired key is too, from switchover
175    /// until decommission (§6.1.6's "both keys valid in parallel" transition
176    /// window) — a data packet under either may legitimately arrive during
177    /// that window (in-flight or retransmitted).
178    pub fn is_key_valid(&self, parity: KeyParity) -> bool {
179        if parity == self.active {
180            return true;
181        }
182        self.switched_over && !self.decommissioned
183    }
184
185    /// Record `n` more packets sent under the active key and return any
186    /// [`KmRefreshEvent`]s newly crossed, in spec order (PreAnnounce,
187    /// Switchover, Decommission). A single call can fire more than one event
188    /// if `n` is large enough to cross multiple thresholds at once.
189    pub fn on_packet_sent(&mut self, n: u64) -> Vec<KmRefreshEvent> {
190        self.total_sent = self.total_sent.saturating_add(n);
191        let mut events = Vec::new();
192        let since_epoch = self.total_sent.saturating_sub(self.epoch_start);
193
194        let pre_announce_at = self
195            .thresholds
196            .refresh_period
197            .saturating_sub(self.thresholds.pre_announcement_period);
198        if !self.pre_announced && since_epoch >= pre_announce_at {
199            self.pre_announced = true;
200            events.push(KmRefreshEvent::PreAnnounce {
201                next_parity: self.active.other(),
202            });
203        }
204
205        if !self.switched_over && since_epoch >= self.thresholds.refresh_period {
206            self.switched_over = true;
207            let new_active = self.active.other();
208            self.active = new_active;
209            // The new epoch starts exactly at the switchover point, so the
210            // decommission threshold below (measured from `epoch_start`) is
211            // `pre_announcement_period` packets *after* switchover — §6.1.6's
212            // `refresh_period + pre_announcement_period`, not
213            // `2 * refresh_period + pre_announcement_period`.
214            self.epoch_start += self.thresholds.refresh_period;
215            events.push(KmRefreshEvent::Switchover { new_active });
216        }
217
218        if self.switched_over && !self.decommissioned {
219            let since_switchover = self.total_sent.saturating_sub(self.epoch_start);
220            if since_switchover >= self.thresholds.pre_announcement_period {
221                self.decommissioned = true;
222                events.push(KmRefreshEvent::Decommission {
223                    retired: self.active.other(),
224                });
225                // Arm the next cycle: `epoch_start` already marks the
226                // current key's start, so the next PreAnnounce/Switchover
227                // are correctly measured from here.
228                self.pre_announced = false;
229                self.switched_over = false;
230                self.decommissioned = false;
231            }
232        }
233
234        events
235    }
236
237    /// Record exactly one packet sent — convenience wrapper over
238    /// [`Self::on_packet_sent`].
239    pub fn tick(&mut self) -> Vec<KmRefreshEvent> {
240        self.on_packet_sent(1)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    const SCALED: KmRefreshThresholds = KmRefreshThresholds {
249        refresh_period: 100,
250        pre_announcement_period: 10,
251    };
252
253    #[test]
254    fn recommended_thresholds_match_spec_values() {
255        assert_eq!(KmRefreshThresholds::RECOMMENDED.refresh_period, 1 << 25);
256        assert_eq!(
257            KmRefreshThresholds::RECOMMENDED.pre_announcement_period,
258            4000
259        );
260    }
261
262    #[test]
263    fn key_parity_alternates_and_labels() {
264        assert_eq!(KeyParity::Even.other(), KeyParity::Odd);
265        assert_eq!(KeyParity::Odd.other(), KeyParity::Even);
266        assert_eq!(KeyParity::Even.to_string(), "even");
267        assert_eq!(KeyParity::Odd.to_string(), "odd");
268    }
269
270    #[test]
271    fn fires_pre_announce_switchover_decommission_in_order() {
272        let mut d = KmRefreshDriver::new(SCALED, KeyParity::Even);
273        assert_eq!(d.active_parity(), KeyParity::Even);
274        assert!(d.is_key_valid(KeyParity::Even));
275        assert!(!d.is_key_valid(KeyParity::Odd));
276
277        // Just before pre-announce: nothing fires.
278        assert_eq!(d.on_packet_sent(89), Vec::new());
279        assert_eq!(d.active_parity(), KeyParity::Even);
280
281        // Crosses 90 (100 - 10): PreAnnounce for the *other* (Odd) parity.
282        assert_eq!(
283            d.on_packet_sent(1),
284            alloc::vec![KmRefreshEvent::PreAnnounce {
285                next_parity: KeyParity::Odd
286            }]
287        );
288        // Active key hasn't changed yet.
289        assert_eq!(d.active_parity(), KeyParity::Even);
290        assert!(d.is_key_valid(KeyParity::Even));
291        assert!(!d.is_key_valid(KeyParity::Odd));
292
293        // Re-crossing the same threshold does not re-fire. (total: 91)
294        assert_eq!(d.on_packet_sent(1), Vec::new());
295
296        // Crosses 100: Switchover to Odd.
297        assert_eq!(d.on_packet_sent(8), Vec::new()); // total 99, not yet
298        assert_eq!(
299            d.on_packet_sent(1), // total 100
300            alloc::vec![KmRefreshEvent::Switchover {
301                new_active: KeyParity::Odd
302            }]
303        );
304        assert_eq!(d.active_parity(), KeyParity::Odd);
305        // Both keys valid during the transition window.
306        assert!(d.is_key_valid(KeyParity::Odd));
307        assert!(d.is_key_valid(KeyParity::Even));
308
309        // Crosses 110 (100 + 10): Decommission the old (Even) key.
310        assert_eq!(d.on_packet_sent(9), Vec::new()); // total 109
311        assert_eq!(
312            d.on_packet_sent(1), // total 110
313            alloc::vec![KmRefreshEvent::Decommission {
314                retired: KeyParity::Even
315            }]
316        );
317        assert_eq!(d.active_parity(), KeyParity::Odd);
318        assert!(d.is_key_valid(KeyParity::Odd));
319        assert!(!d.is_key_valid(KeyParity::Even), "old key must be dropped");
320    }
321
322    #[test]
323    fn large_jump_fires_all_three_events_in_one_call() {
324        let mut d = KmRefreshDriver::new(SCALED, KeyParity::Even);
325        let events = d.on_packet_sent(115);
326        assert_eq!(
327            events,
328            alloc::vec![
329                KmRefreshEvent::PreAnnounce {
330                    next_parity: KeyParity::Odd
331                },
332                KmRefreshEvent::Switchover {
333                    new_active: KeyParity::Odd
334                },
335                KmRefreshEvent::Decommission {
336                    retired: KeyParity::Even
337                },
338            ]
339        );
340        assert_eq!(d.active_parity(), KeyParity::Odd);
341        assert!(!d.is_key_valid(KeyParity::Even));
342    }
343
344    #[test]
345    fn tick_is_a_single_packet_and_rotation_repeats_forever() {
346        let mut d = KmRefreshDriver::new(SCALED, KeyParity::Even);
347        let mut all = Vec::new();
348        for _ in 0..250 {
349            all.extend(d.tick());
350        }
351        assert_eq!(d.total_sent(), 250);
352        // Two full cycles of 100 packets each fit in 250 ticks: expect two
353        // full PreAnnounce/Switchover/Decommission triples, alternating
354        // parity, plus one more PreAnnounce (at 90 packets into the third
355        // 100-packet cycle, i.e. total 290 — not reached at 250) which does
356        // NOT fire yet.
357        let switchovers: Vec<_> = all
358            .iter()
359            .filter(|e| matches!(e, KmRefreshEvent::Switchover { .. }))
360            .collect();
361        assert_eq!(switchovers.len(), 2);
362        assert_eq!(
363            switchovers[0],
364            &KmRefreshEvent::Switchover {
365                new_active: KeyParity::Odd
366            }
367        );
368        assert_eq!(
369            switchovers[1],
370            &KmRefreshEvent::Switchover {
371                new_active: KeyParity::Even
372            }
373        );
374        assert_eq!(d.active_parity(), KeyParity::Even);
375    }
376}