Skip to main content

rustls/
ticketer.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::mem;
4use core::time::Duration;
5use std::sync::{RwLock, RwLockReadGuard};
6use std::time::Instant;
7
8use crate::crypto::TicketProducer;
9use crate::error::Error;
10
11/// A ticketer that has a 'current' sub-ticketer and a single
12/// 'previous' ticketer.  It creates a new ticketer every so
13/// often, demoting the current ticketer.
14pub struct TicketRotator {
15    pub(crate) generator: fn() -> Result<Box<dyn TicketProducer>, Error>,
16    lifetime: Duration,
17    state: RwLock<TicketRotatorState>,
18}
19
20impl TicketRotator {
21    /// Creates a new `TicketRotator`, which rotates through sub-ticketers
22    /// based on the passage of time.
23    ///
24    /// `lifetime` is how long the current ticketer
25    /// is used to generate new tickets.  Tickets are accepted for no
26    /// longer than twice this duration.  This means a given ticket will
27    /// be usable for at least one `lifetime`, and at most two `lifetime`s
28    /// (depending on when its creation falls in the replacement cycle.)
29    ///
30    /// `generator` produces a new [`TicketProducer`] implementation.
31    pub fn new(
32        lifetime: Duration,
33        generator: fn() -> Result<Box<dyn TicketProducer>, Error>,
34    ) -> Result<Self, Error> {
35        Ok(Self {
36            generator,
37            lifetime,
38            state: RwLock::new(TicketRotatorState {
39                current: Some(Generation {
40                    producer: generator()?,
41                    expires_at: Instant::now() + lifetime,
42                }),
43                previous: None,
44            }),
45        })
46    }
47
48    fn encrypt_at(&self, message: &[u8], now: Instant) -> Option<Vec<u8>> {
49        let state = self.maybe_roll(now)?;
50
51        // If we have a current ticketer, use it. We don't need to check its
52        // expiration time; if it would have expired, we would have rolled above.
53        if let Some(current) = &state.current {
54            return current.producer.encrypt(message);
55        }
56
57        // If we don't have a previous ticketer, we can't encrypt.
58        let Some(prev) = &state.previous else {
59            return None;
60        };
61
62        // If the previous ticketer is more than one `lifetime` old, decline to encrypt.
63        if !prev.in_grace_period(now, self.lifetime) {
64            return None;
65        }
66
67        prev.producer.encrypt(message)
68    }
69
70    fn decrypt_at(&self, ciphertext: &[u8], now: Instant) -> Option<Vec<u8>> {
71        let state = self.maybe_roll(now)?;
72
73        // If we have a current ticketer, use it. We don't need to check its
74        // expiration time; if it would have expired, we would have rolled above.
75        if let Some(current) = &state.current {
76            // If decryption fails, we're going to try the previous ticketer below.
77            if let Some(plain) = current.producer.decrypt(ciphertext) {
78                return Some(plain);
79            }
80        }
81
82        // If we don't have a previous ticketer, we can't decrypt.
83        let Some(prev) = &state.previous else {
84            return None;
85        };
86
87        // If the previous ticketer is more than one `lifetime` old, decline to decrypt.
88        if !prev.in_grace_period(now, self.lifetime) {
89            return None;
90        }
91
92        prev.producer.decrypt(ciphertext)
93    }
94
95    /// If it's time, demote the `current` ticketer to `previous` (so it
96    /// does no new encryptions but can do decryption) and replace it
97    /// with a new one.
98    ///
99    /// Calling this regularly will ensure timely key erasure.  Otherwise,
100    /// key erasure will be delayed until the next encrypt/decrypt call.
101    ///
102    /// For efficiency, this is also responsible for locking the state rwlock
103    /// and returning it for read.
104    pub(crate) fn maybe_roll(
105        &self,
106        now: Instant,
107    ) -> Option<RwLockReadGuard<'_, TicketRotatorState>> {
108        // Fast, common, & read-only path in case we do not need to switch
109        // to the next ticketer yet
110        {
111            let read = self.state.read().ok()?;
112            match &read.current {
113                Some(current) if now <= current.expires_at => return Some(read),
114                _ => {}
115            }
116        }
117
118        let mut write = self.state.write().ok()?;
119        if let Some(current) = &write.current {
120            if now <= current.expires_at {
121                // Another thread beat us to it. Nothing to do.
122                drop(write);
123                return self.state.read().ok();
124            }
125        }
126
127        // We need to switch ticketers, and make a new one.
128        // Generate a potential "next" ticketer outside the lock.
129        let next = (self.generator)()
130            .ok()
131            .map(|producer| Generation {
132                producer,
133                expires_at: now + self.lifetime,
134            });
135
136        // Now we have:
137        // - confirmed we need rotation
138        // - confirmed we are the thread that will do it
139        // - successfully made the replacement ticketer
140        let prev = mem::replace(&mut write.current, next);
141        if prev.is_some() {
142            write.previous = prev;
143        }
144        drop(write);
145
146        self.state.read().ok()
147    }
148}
149
150impl TicketProducer for TicketRotator {
151    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
152        self.encrypt_at(message, Instant::now())
153    }
154
155    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
156        self.decrypt_at(ciphertext, Instant::now())
157    }
158
159    fn lifetime(&self) -> Duration {
160        self.lifetime
161    }
162}
163
164impl core::fmt::Debug for TicketRotator {
165    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166        f.debug_struct("TicketRotator")
167            .finish_non_exhaustive()
168    }
169}
170
171pub(crate) struct TicketRotatorState {
172    current: Option<Generation>,
173    previous: Option<Generation>,
174}
175
176struct Generation {
177    producer: Box<dyn TicketProducer>,
178    expires_at: Instant,
179}
180
181impl Generation {
182    fn in_grace_period(&self, now: Instant, lifetime: Duration) -> bool {
183        now <= self.expires_at + lifetime
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use core::sync::atomic::{AtomicU8, Ordering};
190    use core::time::Duration;
191
192    use super::*;
193
194    #[test]
195    fn ticketrotator_switching_test() {
196        let t = TicketRotator::new(Duration::from_secs(1), FakeTicketer::new).unwrap();
197        let now = Instant::now();
198        let cipher1 = t.encrypt(b"ticket 1").unwrap();
199        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
200        {
201            // Trigger new ticketer
202            t.maybe_roll(now + Duration::from_secs(10));
203        }
204        let cipher2 = t.encrypt(b"ticket 2").unwrap();
205        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
206        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
207        {
208            // Trigger new ticketer
209            t.maybe_roll(now + Duration::from_secs(20));
210        }
211        let cipher3 = t.encrypt(b"ticket 3").unwrap();
212        assert!(t.decrypt(&cipher1).is_none());
213        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
214        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
215    }
216
217    #[test]
218    fn ticketrotator_remains_usable_over_temporary_ticketer_creation_failure() {
219        let mut t = TicketRotator::new(Duration::from_secs(1), FakeTicketer::new).unwrap();
220        let expiry = t
221            .state
222            .read()
223            .unwrap()
224            .current
225            .as_ref()
226            .unwrap()
227            .expires_at;
228        let cipher1 = t.encrypt(b"ticket 1").unwrap();
229        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
230        t.generator = fail_generator;
231
232        // Failed new ticketer; this means we still need to rotate.
233        let t1 = expiry;
234        drop(t.maybe_roll(t1));
235        assert!(t.encrypt_at(b"ticket 2", t1).is_some());
236
237        // check post-failure encryption/decryption still works
238        let t2 = expiry + Duration::from_secs(1);
239        let cipher3 = t.encrypt_at(b"ticket 3", t2).unwrap();
240        assert_eq!(t.decrypt_at(&cipher1, t2).unwrap(), b"ticket 1");
241        assert_eq!(t.decrypt_at(&cipher3, t2).unwrap(), b"ticket 3");
242
243        let t3 = expiry + Duration::from_secs(2);
244        assert_eq!(t.encrypt_at(b"ticket 4", t3), None);
245        assert_eq!(t.decrypt_at(&cipher3, t3), None);
246
247        // do the rotation for real
248        t.generator = FakeTicketer::new;
249        let t4 = expiry + Duration::from_secs(3);
250        drop(t.maybe_roll(t4));
251
252        let t5 = expiry + Duration::from_secs(4);
253        let cipher5 = t.encrypt_at(b"ticket 5", t5).unwrap();
254        assert!(t.decrypt_at(&cipher1, t5).is_none());
255        assert!(t.decrypt_at(&cipher3, t5).is_none());
256        assert_eq!(t.decrypt_at(&cipher5, t5).unwrap(), b"ticket 5");
257
258        // Cover the case where both ticketers are unavailable
259        t.generator = fail_generator;
260        let mut write = t.state.write().unwrap();
261        write.current = None;
262        write.previous = None;
263        drop(write);
264        assert!(t.encrypt(b"ticket 6").is_none());
265    }
266
267    #[derive(Debug)]
268    struct FakeTicketer {
269        generation: u8,
270    }
271
272    impl FakeTicketer {
273        #[expect(clippy::new_ret_no_self)]
274        fn new() -> Result<Box<dyn TicketProducer>, Error> {
275            Ok(Box::new(Self {
276                generation: std::dbg!(FAKE_GEN.fetch_add(1, Ordering::SeqCst)),
277            }))
278        }
279    }
280
281    impl TicketProducer for FakeTicketer {
282        fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
283            let mut v = Vec::with_capacity(1 + message.len());
284            v.push(self.generation);
285            v.extend(
286                message
287                    .iter()
288                    .copied()
289                    .map(|b| b ^ self.generation),
290            );
291            Some(v)
292        }
293
294        fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
295            if ciphertext.first()? != &self.generation {
296                return None;
297            }
298
299            Some(
300                ciphertext[1..]
301                    .iter()
302                    .copied()
303                    .map(|b| b ^ self.generation)
304                    .collect(),
305            )
306        }
307
308        fn lifetime(&self) -> Duration {
309            Duration::ZERO // Left to the rotator
310        }
311    }
312
313    static FAKE_GEN: AtomicU8 = AtomicU8::new(0);
314
315    fn fail_generator() -> Result<Box<dyn TicketProducer>, Error> {
316        Err(Error::FailedToGetRandomBytes)
317    }
318}