rustls/
ticketer.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::mem;
4
5use pki_types::UnixTime;
6
7use crate::lock::{Mutex, MutexGuard};
8use crate::server::ProducesTickets;
9#[cfg(not(feature = "std"))]
10use crate::time_provider::TimeProvider;
11use crate::{rand, Error};
12
13#[derive(Debug)]
14pub(crate) struct TicketSwitcherState {
15    next: Option<Box<dyn ProducesTickets>>,
16    current: Box<dyn ProducesTickets>,
17    previous: Option<Box<dyn ProducesTickets>>,
18    next_switch_time: u64,
19}
20
21/// A ticketer that has a 'current' sub-ticketer and a single
22/// 'previous' ticketer.  It creates a new ticketer every so
23/// often, demoting the current ticketer.
24#[cfg_attr(feature = "std", derive(Debug))]
25pub struct TicketSwitcher {
26    pub(crate) generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
27    lifetime: u32,
28    state: Mutex<TicketSwitcherState>,
29    #[cfg(not(feature = "std"))]
30    time_provider: &'static dyn TimeProvider,
31}
32
33impl TicketSwitcher {
34    /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
35    /// based on the passage of time.
36    ///
37    /// `lifetime` is in seconds, and is how long the current ticketer
38    /// is used to generate new tickets.  Tickets are accepted for no
39    /// longer than twice this duration.  `generator` produces a new
40    /// `ProducesTickets` implementation.
41    #[cfg(feature = "std")]
42    pub fn new(
43        lifetime: u32,
44        generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
45    ) -> Result<Self, Error> {
46        Ok(Self {
47            generator,
48            lifetime,
49            state: Mutex::new(TicketSwitcherState {
50                next: Some(generator()?),
51                current: generator()?,
52                previous: None,
53                next_switch_time: UnixTime::now()
54                    .as_secs()
55                    .saturating_add(u64::from(lifetime)),
56            }),
57        })
58    }
59
60    /// Creates a new `TicketSwitcher`, which rotates through sub-ticketers
61    /// based on the passage of time.
62    ///
63    /// `lifetime` is in seconds, and is how long the current ticketer
64    /// is used to generate new tickets.  Tickets are accepted for no
65    /// longer than twice this duration.  `generator` produces a new
66    /// `ProducesTickets` implementation.
67    #[cfg(not(feature = "std"))]
68    pub fn new<M: crate::lock::MakeMutex>(
69        lifetime: u32,
70        generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
71        time_provider: &'static dyn TimeProvider,
72    ) -> Result<Self, Error> {
73        Ok(Self {
74            generator,
75            lifetime,
76            state: Mutex::new::<M>(TicketSwitcherState {
77                next: Some(generator()?),
78                current: generator()?,
79                previous: None,
80                next_switch_time: time_provider
81                    .current_time()
82                    .unwrap()
83                    .as_secs()
84                    .saturating_add(u64::from(lifetime)),
85            }),
86            time_provider,
87        })
88    }
89
90    /// If it's time, demote the `current` ticketer to `previous` (so it
91    /// does no new encryptions but can do decryption) and use next for a
92    /// new `current` ticketer.
93    ///
94    /// Calling this regularly will ensure timely key erasure.  Otherwise,
95    /// key erasure will be delayed until the next encrypt/decrypt call.
96    ///
97    /// For efficiency, this is also responsible for locking the state mutex
98    /// and returning the mutexguard.
99    pub(crate) fn maybe_roll(&self, now: UnixTime) -> Option<MutexGuard<'_, TicketSwitcherState>> {
100        // The code below aims to make switching as efficient as possible
101        // in the common case that the generator never fails. To achieve this
102        // we run the following steps:
103        //  1. If no switch is necessary, just return the mutexguard
104        //  2. Shift over all of the ticketers (so current becomes previous,
105        //     and next becomes current). After this, other threads can
106        //     start using the new current ticketer.
107        //  3. unlock mutex and generate new ticketer.
108        //  4. Place new ticketer in next and return current
109        //
110        // There are a few things to note here. First, we don't check whether
111        // a new switch might be needed in step 4, even though, due to locking
112        // and entropy collection, significant amounts of time may have passed.
113        // This is to guarantee that the thread doing the switch will eventually
114        // make progress.
115        //
116        // Second, because next may be None, step 2 can fail. In that case
117        // we enter a recovery mode where we generate 2 new ticketers, one for
118        // next and one for the current ticketer. We then take the mutex a
119        // second time and redo the time check to see if a switch is still
120        // necessary.
121        //
122        // This somewhat convoluted approach ensures good availability of the
123        // mutex, by ensuring that the state is usable and the mutex not held
124        // during generation. It also ensures that, so long as the inner
125        // ticketer never generates panics during encryption/decryption,
126        // we are guaranteed to never panic when holding the mutex.
127
128        let now = now.as_secs();
129        let mut are_recovering = false; // Are we recovering from previous failure?
130        {
131            // Scope the mutex so we only take it for as long as needed
132            let mut state = self.state.lock()?;
133
134            // Fast path in case we do not need to switch to the next ticketer yet
135            if now <= state.next_switch_time {
136                return Some(state);
137            }
138
139            // Make the switch, or mark for recovery if not possible
140            if let Some(next) = state.next.take() {
141                state.previous = Some(mem::replace(&mut state.current, next));
142                state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
143            } else {
144                are_recovering = true;
145            }
146        }
147
148        // We always need a next, so generate it now
149        let next = (self.generator)().ok()?;
150        if !are_recovering {
151            // Normal path, generate new next and place it in the state
152            let mut state = self.state.lock()?;
153            state.next = Some(next);
154            Some(state)
155        } else {
156            // Recovering, generate also a new current ticketer, and modify state
157            // as needed. (we need to redo the time check, otherwise this might
158            // result in very rapid switching of ticketers)
159            let new_current = (self.generator)().ok()?;
160            let mut state = self.state.lock()?;
161            state.next = Some(next);
162            if now > state.next_switch_time {
163                state.previous = Some(mem::replace(&mut state.current, new_current));
164                state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
165            }
166            Some(state)
167        }
168    }
169}
170
171impl ProducesTickets for TicketSwitcher {
172    fn lifetime(&self) -> u32 {
173        self.lifetime * 2
174    }
175
176    fn enabled(&self) -> bool {
177        true
178    }
179
180    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
181        #[cfg(feature = "std")]
182        let now = UnixTime::now();
183        #[cfg(not(feature = "std"))]
184        let now = self
185            .time_provider
186            .current_time()
187            .unwrap();
188
189        self.maybe_roll(now)?
190            .current
191            .encrypt(message)
192    }
193
194    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
195        #[cfg(feature = "std")]
196        let now = UnixTime::now();
197        #[cfg(not(feature = "std"))]
198        let now = self
199            .time_provider
200            .current_time()
201            .unwrap();
202
203        let state = self.maybe_roll(now)?;
204
205        // Decrypt with the current key; if that fails, try with the previous.
206        state
207            .current
208            .decrypt(ciphertext)
209            .or_else(|| {
210                state
211                    .previous
212                    .as_ref()
213                    .and_then(|previous| previous.decrypt(ciphertext))
214            })
215    }
216}
217
218#[cfg(not(feature = "std"))]
219impl core::fmt::Debug for TicketSwitcher {
220    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
221        f.debug_struct("TicketSwitcher")
222            .field("generator", &self.generator)
223            .field("lifetime", &self.lifetime)
224            .field("state", &**self.state.lock().unwrap())
225            .finish()
226    }
227}