rustls/
ticketer.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::mem;
4#[cfg(feature = "std")]
5use std::sync::{RwLock, RwLockReadGuard};
6
7use pki_types::UnixTime;
8
9use crate::lock::{Mutex, MutexGuard};
10use crate::server::ProducesTickets;
11#[cfg(not(feature = "std"))]
12use crate::time_provider::TimeProvider;
13use crate::{Error, rand};
14
15#[derive(Debug)]
16pub(crate) struct TicketSwitcherState {
17    next: Option<Box<dyn ProducesTickets>>,
18    current: Box<dyn ProducesTickets>,
19    previous: Option<Box<dyn ProducesTickets>>,
20    next_switch_time: u64,
21}
22
23/// A ticketer that has a 'current' sub-ticketer and a single
24/// 'previous' ticketer.  It creates a new ticketer every so
25/// often, demoting the current ticketer.
26#[cfg_attr(feature = "std", derive(Debug))]
27pub struct TicketSwitcher {
28    pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
29    lifetime: u32,
30    state: Mutex<TicketSwitcherState>,
31    #[cfg(not(feature = "std"))]
32    time_provider: &'static dyn TimeProvider,
33}
34
35impl TicketSwitcher {
36    /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
37    /// based on the passage of time.
38    ///
39    /// `lifetime` is in seconds, and is how long the current ticketer
40    /// is used to generate new tickets.  Tickets are accepted for no
41    /// longer than twice this duration.  `generator` produces a new
42    /// `ProducesTickets` implementation.
43    #[cfg(feature = "std")]
44    #[deprecated(note = "use TicketRotator instead")]
45    pub fn new(
46        lifetime: u32,
47        generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
48    ) -> Result<Self, Error> {
49        Ok(Self {
50            generator,
51            lifetime,
52            state: Mutex::new(TicketSwitcherState {
53                next: Some(generator()?),
54                current: generator()?,
55                previous: None,
56                next_switch_time: UnixTime::now()
57                    .as_secs()
58                    .saturating_add(u64::from(lifetime)),
59            }),
60        })
61    }
62
63    /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
64    /// based on the passage of time.
65    ///
66    /// `lifetime` is in seconds, and is how long the current ticketer
67    /// is used to generate new tickets.  Tickets are accepted for no
68    /// longer than twice this duration.  `generator` produces a new
69    /// `ProducesTickets` implementation.
70    #[cfg(not(feature = "std"))]
71    pub fn new<M: crate::lock::MakeMutex>(
72        lifetime: u32,
73        generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
74        time_provider: &'static dyn TimeProvider,
75    ) -> Result<Self, Error> {
76        Ok(Self {
77            generator,
78            lifetime,
79            state: Mutex::new::<M>(TicketSwitcherState {
80                next: Some(generator()?),
81                current: generator()?,
82                previous: None,
83                next_switch_time: time_provider
84                    .current_time()
85                    .unwrap()
86                    .as_secs()
87                    .saturating_add(u64::from(lifetime)),
88            }),
89            time_provider,
90        })
91    }
92
93    /// If it's time, demote the `current` ticketer to `previous` (so it
94    /// does no new encryptions but can do decryption) and use next for a
95    /// new `current` ticketer.
96    ///
97    /// Calling this regularly will ensure timely key erasure.  Otherwise,
98    /// key erasure will be delayed until the next encrypt/decrypt call.
99    ///
100    /// For efficiency, this is also responsible for locking the state mutex
101    /// and returning the mutexguard.
102    pub(crate) fn maybe_roll(&self, now: UnixTime) -> Option<MutexGuard<'_, TicketSwitcherState>> {
103        // The code below aims to make switching as efficient as possible
104        // in the common case that the generator never fails. To achieve this
105        // we run the following steps:
106        //  1. If no switch is necessary, just return the mutexguard
107        //  2. Shift over all of the ticketers (so current becomes previous,
108        //     and next becomes current). After this, other threads can
109        //     start using the new current ticketer.
110        //  3. unlock mutex and generate new ticketer.
111        //  4. Place new ticketer in next and return current
112        //
113        // There are a few things to note here. First, we don't check whether
114        // a new switch might be needed in step 4, even though, due to locking
115        // and entropy collection, significant amounts of time may have passed.
116        // This is to guarantee that the thread doing the switch will eventually
117        // make progress.
118        //
119        // Second, because next may be None, step 2 can fail. In that case
120        // we enter a recovery mode where we generate 2 new ticketers, one for
121        // next and one for the current ticketer. We then take the mutex a
122        // second time and redo the time check to see if a switch is still
123        // necessary.
124        //
125        // This somewhat convoluted approach ensures good availability of the
126        // mutex, by ensuring that the state is usable and the mutex not held
127        // during generation. It also ensures that, so long as the inner
128        // ticketer never generates panics during encryption/decryption,
129        // we are guaranteed to never panic when holding the mutex.
130
131        let now = now.as_secs();
132        let mut are_recovering = false; // Are we recovering from previous failure?
133        {
134            // Scope the mutex so we only take it for as long as needed
135            let mut state = self.state.lock()?;
136
137            // Fast path in case we do not need to switch to the next ticketer yet
138            if now <= state.next_switch_time {
139                return Some(state);
140            }
141
142            // Make the switch, or mark for recovery if not possible
143            match state.next.take() {
144                Some(next) => {
145                    state.previous = Some(mem::replace(&mut state.current, next));
146                    state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
147                }
148                _ => are_recovering = true,
149            }
150        }
151
152        // We always need a next, so generate it now
153        let next = (self.generator)().ok()?;
154        if !are_recovering {
155            // Normal path, generate new next and place it in the state
156            let mut state = self.state.lock()?;
157            state.next = Some(next);
158            Some(state)
159        } else {
160            // Recovering, generate also a new current ticketer, and modify state
161            // as needed. (we need to redo the time check, otherwise this might
162            // result in very rapid switching of ticketers)
163            let new_current = (self.generator)().ok()?;
164            let mut state = self.state.lock()?;
165            state.next = Some(next);
166            if now > state.next_switch_time {
167                state.previous = Some(mem::replace(&mut state.current, new_current));
168                state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
169            }
170            Some(state)
171        }
172    }
173}
174
175impl ProducesTickets for TicketSwitcher {
176    fn lifetime(&self) -> u32 {
177        self.lifetime * 2
178    }
179
180    fn enabled(&self) -> bool {
181        true
182    }
183
184    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
185        #[cfg(feature = "std")]
186        let now = UnixTime::now();
187        #[cfg(not(feature = "std"))]
188        let now = self
189            .time_provider
190            .current_time()
191            .unwrap();
192
193        self.maybe_roll(now)?
194            .current
195            .encrypt(message)
196    }
197
198    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
199        #[cfg(feature = "std")]
200        let now = UnixTime::now();
201        #[cfg(not(feature = "std"))]
202        let now = self
203            .time_provider
204            .current_time()
205            .unwrap();
206
207        let state = self.maybe_roll(now)?;
208
209        // Decrypt with the current key; if that fails, try with the previous.
210        state
211            .current
212            .decrypt(ciphertext)
213            .or_else(|| {
214                state
215                    .previous
216                    .as_ref()
217                    .and_then(|previous| previous.decrypt(ciphertext))
218            })
219    }
220}
221
222#[cfg(not(feature = "std"))]
223impl core::fmt::Debug for TicketSwitcher {
224    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
225        f.debug_struct("TicketSwitcher")
226            .field("generator", &self.generator)
227            .field("lifetime", &self.lifetime)
228            .field("state", &**self.state.lock().unwrap())
229            .finish()
230    }
231}
232
233#[cfg(feature = "std")]
234#[derive(Debug)]
235pub(crate) struct TicketRotatorState {
236    current: Box<dyn ProducesTickets>,
237    previous: Option<Box<dyn ProducesTickets>>,
238    next_switch_time: u64,
239}
240
241/// A ticketer that has a 'current' sub-ticketer and a single
242/// 'previous' ticketer.  It creates a new ticketer every so
243/// often, demoting the current ticketer.
244#[cfg(feature = "std")]
245pub struct TicketRotator {
246    pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
247    lifetime: u32,
248    state: RwLock<TicketRotatorState>,
249}
250
251#[cfg(feature = "std")]
252impl TicketRotator {
253    /// Creates a new `TicketRotator`, which rotates through sub-ticketers
254    /// based on the passage of time.
255    ///
256    /// `lifetime` is in seconds, and is how long the current ticketer
257    /// is used to generate new tickets.  Tickets are accepted for no
258    /// longer than twice this duration.  `generator` produces a new
259    /// `ProducesTickets` implementation.
260    pub fn new(
261        lifetime: u32,
262        generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
263    ) -> Result<Self, Error> {
264        Ok(Self {
265            generator,
266            lifetime,
267            state: RwLock::new(TicketRotatorState {
268                current: generator()?,
269                previous: None,
270                next_switch_time: UnixTime::now()
271                    .as_secs()
272                    .saturating_add(u64::from(lifetime)),
273            }),
274        })
275    }
276
277    /// If it's time, demote the `current` ticketer to `previous` (so it
278    /// does no new encryptions but can do decryption) and replace it
279    /// with a new one.
280    ///
281    /// Calling this regularly will ensure timely key erasure.  Otherwise,
282    /// key erasure will be delayed until the next encrypt/decrypt call.
283    ///
284    /// For efficiency, this is also responsible for locking the state rwlock
285    /// and returning it for read.
286    pub(crate) fn maybe_roll(
287        &self,
288        now: UnixTime,
289    ) -> Option<RwLockReadGuard<'_, TicketRotatorState>> {
290        let now = now.as_secs();
291
292        // Fast, common, & read-only path in case we do not need to switch
293        // to the next ticketer yet
294        {
295            let read = self.state.read().ok()?;
296
297            if now <= read.next_switch_time {
298                return Some(read);
299            }
300        }
301
302        // We need to switch ticketers, and make a new one.
303        // Generate a potential "next" ticketer outside the lock.
304        let next = (self.generator)().ok()?;
305
306        let mut write = self.state.write().ok()?;
307
308        if now <= write.next_switch_time {
309            // Another thread beat us to it.  Nothing to do.
310            drop(write);
311
312            return self.state.read().ok();
313        }
314
315        // Now we have:
316        // - confirmed we need rotation
317        // - confirmed we are the thread that will do it
318        // - successfully made the replacement ticketer
319        write.previous = Some(mem::replace(&mut write.current, next));
320        write.next_switch_time = now.saturating_add(u64::from(self.lifetime));
321        drop(write);
322
323        self.state.read().ok()
324    }
325}
326
327#[cfg(feature = "std")]
328impl ProducesTickets for TicketRotator {
329    fn lifetime(&self) -> u32 {
330        self.lifetime * 2
331    }
332
333    fn enabled(&self) -> bool {
334        true
335    }
336
337    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
338        self.maybe_roll(UnixTime::now())?
339            .current
340            .encrypt(message)
341    }
342
343    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
344        let state = self.maybe_roll(UnixTime::now())?;
345
346        // Decrypt with the current key; if that fails, try with the previous.
347        state
348            .current
349            .decrypt(ciphertext)
350            .or_else(|| {
351                state
352                    .previous
353                    .as_ref()
354                    .and_then(|previous| previous.decrypt(ciphertext))
355            })
356    }
357}
358
359#[cfg(feature = "std")]
360impl core::fmt::Debug for TicketRotator {
361    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
362        f.debug_struct("TicketRotator")
363            .finish_non_exhaustive()
364    }
365}