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