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::{rand, Error};
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            if let Some(next) = state.next.take() {
144                state.previous = Some(mem::replace(&mut state.current, next));
145                state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
146            } else {
147                are_recovering = true;
148            }
149        }
150
151        // We always need a next, so generate it now
152        let next = (self.generator)().ok()?;
153        if !are_recovering {
154            // Normal path, generate new next and place it in the state
155            let mut state = self.state.lock()?;
156            state.next = Some(next);
157            Some(state)
158        } else {
159            // Recovering, generate also a new current ticketer, and modify state
160            // as needed. (we need to redo the time check, otherwise this might
161            // result in very rapid switching of ticketers)
162            let new_current = (self.generator)().ok()?;
163            let mut state = self.state.lock()?;
164            state.next = Some(next);
165            if now > state.next_switch_time {
166                state.previous = Some(mem::replace(&mut state.current, new_current));
167                state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
168            }
169            Some(state)
170        }
171    }
172}
173
174impl ProducesTickets for TicketSwitcher {
175    fn lifetime(&self) -> u32 {
176        self.lifetime * 2
177    }
178
179    fn enabled(&self) -> bool {
180        true
181    }
182
183    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
184        #[cfg(feature = "std")]
185        let now = UnixTime::now();
186        #[cfg(not(feature = "std"))]
187        let now = self
188            .time_provider
189            .current_time()
190            .unwrap();
191
192        self.maybe_roll(now)?
193            .current
194            .encrypt(message)
195    }
196
197    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
198        #[cfg(feature = "std")]
199        let now = UnixTime::now();
200        #[cfg(not(feature = "std"))]
201        let now = self
202            .time_provider
203            .current_time()
204            .unwrap();
205
206        let state = self.maybe_roll(now)?;
207
208        // Decrypt with the current key; if that fails, try with the previous.
209        state
210            .current
211            .decrypt(ciphertext)
212            .or_else(|| {
213                state
214                    .previous
215                    .as_ref()
216                    .and_then(|previous| previous.decrypt(ciphertext))
217            })
218    }
219}
220
221#[cfg(not(feature = "std"))]
222impl core::fmt::Debug for TicketSwitcher {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        f.debug_struct("TicketSwitcher")
225            .field("generator", &self.generator)
226            .field("lifetime", &self.lifetime)
227            .field("state", &**self.state.lock().unwrap())
228            .finish()
229    }
230}
231
232#[cfg(feature = "std")]
233#[derive(Debug)]
234pub(crate) struct TicketRotatorState {
235    current: Box<dyn ProducesTickets>,
236    previous: Option<Box<dyn ProducesTickets>>,
237    next_switch_time: u64,
238}
239
240/// A ticketer that has a 'current' sub-ticketer and a single
241/// 'previous' ticketer.  It creates a new ticketer every so
242/// often, demoting the current ticketer.
243#[cfg(feature = "std")]
244pub struct TicketRotator {
245    pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
246    lifetime: u32,
247    state: RwLock<TicketRotatorState>,
248}
249
250#[cfg(feature = "std")]
251impl TicketRotator {
252    /// Creates a new `TicketRotator`, which rotates through sub-ticketers
253    /// based on the passage of time.
254    ///
255    /// `lifetime` is in seconds, and is how long the current ticketer
256    /// is used to generate new tickets.  Tickets are accepted for no
257    /// longer than twice this duration.  `generator` produces a new
258    /// `ProducesTickets` implementation.
259    pub fn new(
260        lifetime: u32,
261        generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
262    ) -> Result<Self, Error> {
263        Ok(Self {
264            generator,
265            lifetime,
266            state: RwLock::new(TicketRotatorState {
267                current: generator()?,
268                previous: None,
269                next_switch_time: UnixTime::now()
270                    .as_secs()
271                    .saturating_add(u64::from(lifetime)),
272            }),
273        })
274    }
275
276    /// If it's time, demote the `current` ticketer to `previous` (so it
277    /// does no new encryptions but can do decryption) and replace it
278    /// with a new one.
279    ///
280    /// Calling this regularly will ensure timely key erasure.  Otherwise,
281    /// key erasure will be delayed until the next encrypt/decrypt call.
282    ///
283    /// For efficiency, this is also responsible for locking the state rwlock
284    /// and returning it for read.
285    pub(crate) fn maybe_roll(
286        &self,
287        now: UnixTime,
288    ) -> Option<RwLockReadGuard<'_, TicketRotatorState>> {
289        let now = now.as_secs();
290
291        // Fast, common, & read-only path in case we do not need to switch
292        // to the next ticketer yet
293        {
294            let read = self.state.read().ok()?;
295
296            if now <= read.next_switch_time {
297                return Some(read);
298            }
299        }
300
301        // We need to switch ticketers, and make a new one.
302        // Generate a potential "next" ticketer outside the lock.
303        let next = (self.generator)().ok()?;
304
305        let mut write = self.state.write().ok()?;
306
307        if now <= write.next_switch_time {
308            // Another thread beat us to it.  Nothing to do.
309            drop(write);
310
311            return self.state.read().ok();
312        }
313
314        // Now we have:
315        // - confirmed we need rotation
316        // - confirmed we are the thread that will do it
317        // - successfully made the replacement ticketer
318        write.previous = Some(mem::replace(&mut write.current, next));
319        write.next_switch_time = now.saturating_add(u64::from(self.lifetime));
320        drop(write);
321
322        self.state.read().ok()
323    }
324}
325
326#[cfg(feature = "std")]
327impl ProducesTickets for TicketRotator {
328    fn lifetime(&self) -> u32 {
329        self.lifetime * 2
330    }
331
332    fn enabled(&self) -> bool {
333        true
334    }
335
336    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
337        self.maybe_roll(UnixTime::now())?
338            .current
339            .encrypt(message)
340    }
341
342    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
343        let state = self.maybe_roll(UnixTime::now())?;
344
345        // Decrypt with the current key; if that fails, try with the previous.
346        state
347            .current
348            .decrypt(ciphertext)
349            .or_else(|| {
350                state
351                    .previous
352                    .as_ref()
353                    .and_then(|previous| previous.decrypt(ciphertext))
354            })
355    }
356}
357
358#[cfg(feature = "std")]
359impl core::fmt::Debug for TicketRotator {
360    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
361        f.debug_struct("TicketRotator")
362            .finish_non_exhaustive()
363    }
364}