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