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