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}