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}