rustls/crypto/ring/
ticketer.rs

1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::fmt;
4use core::fmt::{Debug, Formatter};
5use core::sync::atomic::{AtomicUsize, Ordering};
6
7use subtle::ConstantTimeEq;
8
9use super::ring_like::aead;
10use super::ring_like::rand::{SecureRandom, SystemRandom};
11use crate::error::Error;
12#[cfg(debug_assertions)]
13use crate::log::debug;
14use crate::polyfill::try_split_at;
15use crate::rand::GetRandomFailed;
16use crate::server::ProducesTickets;
17use crate::sync::Arc;
18
19/// A concrete, safe ticket creation mechanism.
20pub struct Ticketer {}
21
22impl Ticketer {
23    /// Make the recommended `Ticketer`.  This produces tickets
24    /// with a 12 hour life and randomly generated keys.
25    ///
26    /// The encryption mechanism used is Chacha20Poly1305.
27    #[cfg(feature = "std")]
28    pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
29        Ok(Arc::new(crate::ticketer::TicketRotator::new(
30            6 * 60 * 60,
31            make_ticket_generator,
32        )?))
33    }
34}
35
36fn make_ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
37    Ok(Box::new(AeadTicketer::new()?))
38}
39
40/// This is a `ProducesTickets` implementation which uses
41/// any *ring* `aead::Algorithm` to encrypt and authentication
42/// the ticket payload.  It does not enforce any lifetime
43/// constraint.
44struct AeadTicketer {
45    alg: &'static aead::Algorithm,
46    key: aead::LessSafeKey,
47    key_name: [u8; 16],
48    lifetime: u32,
49
50    /// Tracks the largest ciphertext produced by `encrypt`, and
51    /// uses it to early-reject `decrypt` queries that are too long.
52    ///
53    /// Accepting excessively long ciphertexts means a "Partitioning
54    /// Oracle Attack" (see <https://eprint.iacr.org/2020/1491.pdf>)
55    /// can be more efficient, though also note that these are thought
56    /// to be cryptographically hard if the key is full-entropy (as it
57    /// is here).
58    maximum_ciphertext_len: AtomicUsize,
59}
60
61impl AeadTicketer {
62    fn new() -> Result<Self, GetRandomFailed> {
63        let mut key = [0u8; 32];
64        SystemRandom::new()
65            .fill(&mut key)
66            .map_err(|_| GetRandomFailed)?;
67
68        let key = aead::UnboundKey::new(TICKETER_AEAD, &key).unwrap();
69
70        let mut key_name = [0u8; 16];
71        SystemRandom::new()
72            .fill(&mut key_name)
73            .map_err(|_| GetRandomFailed)?;
74
75        Ok(Self {
76            alg: TICKETER_AEAD,
77            key: aead::LessSafeKey::new(key),
78            key_name,
79            lifetime: 60 * 60 * 12,
80            maximum_ciphertext_len: AtomicUsize::new(0),
81        })
82    }
83}
84
85impl ProducesTickets for AeadTicketer {
86    fn enabled(&self) -> bool {
87        true
88    }
89
90    fn lifetime(&self) -> u32 {
91        self.lifetime
92    }
93
94    /// Encrypt `message` and return the ciphertext.
95    fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
96        // Random nonce, because a counter is a privacy leak.
97        let mut nonce_buf = [0u8; 12];
98        SystemRandom::new()
99            .fill(&mut nonce_buf)
100            .ok()?;
101        let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
102        let aad = aead::Aad::from(self.key_name);
103
104        // ciphertext structure is:
105        // key_name: [u8; 16]
106        // nonce: [u8; 12]
107        // message: [u8, _]
108        // tag: [u8; 16]
109
110        let mut ciphertext = Vec::with_capacity(
111            self.key_name.len() + nonce_buf.len() + message.len() + self.key.algorithm().tag_len(),
112        );
113        ciphertext.extend(self.key_name);
114        ciphertext.extend(nonce_buf);
115        ciphertext.extend(message);
116        let ciphertext = self
117            .key
118            .seal_in_place_separate_tag(
119                nonce,
120                aad,
121                &mut ciphertext[self.key_name.len() + nonce_buf.len()..],
122            )
123            .map(|tag| {
124                ciphertext.extend(tag.as_ref());
125                ciphertext
126            })
127            .ok()?;
128
129        self.maximum_ciphertext_len
130            .fetch_max(ciphertext.len(), Ordering::SeqCst);
131        Some(ciphertext)
132    }
133
134    /// Decrypt `ciphertext` and recover the original message.
135    fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
136        if ciphertext.len()
137            > self
138                .maximum_ciphertext_len
139                .load(Ordering::SeqCst)
140        {
141            #[cfg(debug_assertions)]
142            debug!("rejected over-length ticket");
143            return None;
144        }
145
146        let (alleged_key_name, ciphertext) = try_split_at(ciphertext, self.key_name.len())?;
147
148        let (nonce, ciphertext) = try_split_at(ciphertext, self.alg.nonce_len())?;
149
150        // checking the key_name is the expected one, *and* then putting it into the
151        // additionally authenticated data is duplicative.  this check quickly rejects
152        // tickets for a different ticketer (see `TicketSwitcher`), while including it
153        // in the AAD ensures it is authenticated independent of that check and that
154        // any attempted attack on the integrity such as [^1] must happen for each
155        // `key_label`, not over a population of potential keys.  this approach
156        // is overall similar to [^2].
157        //
158        // [^1]: https://eprint.iacr.org/2020/1491.pdf
159        // [^2]: "Authenticated Encryption with Key Identification", fig 6
160        //       <https://eprint.iacr.org/2022/1680.pdf>
161        if ConstantTimeEq::ct_ne(&self.key_name[..], alleged_key_name).into() {
162            #[cfg(debug_assertions)]
163            debug!("rejected ticket with wrong ticket_name");
164            return None;
165        }
166
167        // This won't fail since `nonce` has the required length.
168        let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?;
169
170        let mut out = Vec::from(ciphertext);
171
172        let plain_len = self
173            .key
174            .open_in_place(nonce, aead::Aad::from(alleged_key_name), &mut out)
175            .ok()?
176            .len();
177        out.truncate(plain_len);
178
179        Some(out)
180    }
181}
182
183impl Debug for AeadTicketer {
184    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
185        // Note: we deliberately omit the key from the debug output.
186        f.debug_struct("AeadTicketer")
187            .field("alg", &self.alg)
188            .field("lifetime", &self.lifetime)
189            .finish()
190    }
191}
192
193static TICKETER_AEAD: &aead::Algorithm = &aead::CHACHA20_POLY1305;
194
195#[cfg(test)]
196mod tests {
197    use core::time::Duration;
198
199    use pki_types::UnixTime;
200
201    use super::*;
202
203    #[test]
204    fn basic_pairwise_test() {
205        let t = Ticketer::new().unwrap();
206        assert!(t.enabled());
207        let cipher = t.encrypt(b"hello world").unwrap();
208        let plain = t.decrypt(&cipher).unwrap();
209        assert_eq!(plain, b"hello world");
210    }
211
212    #[test]
213    fn refuses_decrypt_before_encrypt() {
214        let t = Ticketer::new().unwrap();
215        assert_eq!(t.decrypt(b"hello"), None);
216    }
217
218    #[test]
219    fn refuses_decrypt_larger_than_largest_encryption() {
220        let t = Ticketer::new().unwrap();
221        let mut cipher = t.encrypt(b"hello world").unwrap();
222        assert_eq!(t.decrypt(&cipher), Some(b"hello world".to_vec()));
223
224        // obviously this would never work anyway, but this
225        // and `cannot_decrypt_before_encrypt` exercise the
226        // first branch in `decrypt()`
227        cipher.push(0);
228        assert_eq!(t.decrypt(&cipher), None);
229    }
230
231    #[test]
232    fn ticketrotator_switching_test() {
233        let t = Arc::new(crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap());
234        let now = UnixTime::now();
235        let cipher1 = t.encrypt(b"ticket 1").unwrap();
236        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
237        {
238            // Trigger new ticketer
239            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
240                now.as_secs() + 10,
241            )));
242        }
243        let cipher2 = t.encrypt(b"ticket 2").unwrap();
244        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
245        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
246        {
247            // Trigger new ticketer
248            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
249                now.as_secs() + 20,
250            )));
251        }
252        let cipher3 = t.encrypt(b"ticket 3").unwrap();
253        assert!(t.decrypt(&cipher1).is_none());
254        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
255        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
256    }
257
258    #[test]
259    fn ticketrotator_remains_usable_over_temporary_ticketer_creation_failure() {
260        let mut t = crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap();
261        let now = UnixTime::now();
262        let cipher1 = t.encrypt(b"ticket 1").unwrap();
263        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
264        t.generator = fail_generator;
265        {
266            // Failed new ticketer; this means we still need to
267            // rotate.
268            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
269                now.as_secs() + 10,
270            )));
271        }
272
273        // check post-failure encryption/decryption still works
274        let cipher2 = t.encrypt(b"ticket 2").unwrap();
275        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
276        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
277
278        // do the rotation for real
279        t.generator = make_ticket_generator;
280        {
281            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
282                now.as_secs() + 20,
283            )));
284        }
285        let cipher3 = t.encrypt(b"ticket 3").unwrap();
286        assert!(t.decrypt(&cipher1).is_some());
287        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
288        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
289    }
290
291    #[test]
292    fn ticketswitcher_switching_test() {
293        #[expect(deprecated)]
294        let t = Arc::new(crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap());
295        let now = UnixTime::now();
296        let cipher1 = t.encrypt(b"ticket 1").unwrap();
297        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
298        {
299            // Trigger new ticketer
300            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
301                now.as_secs() + 10,
302            )));
303        }
304        let cipher2 = t.encrypt(b"ticket 2").unwrap();
305        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
306        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
307        {
308            // Trigger new ticketer
309            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
310                now.as_secs() + 20,
311            )));
312        }
313        let cipher3 = t.encrypt(b"ticket 3").unwrap();
314        assert!(t.decrypt(&cipher1).is_none());
315        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
316        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
317    }
318
319    #[test]
320    fn ticketswitcher_recover_test() {
321        #[expect(deprecated)]
322        let mut t = crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap();
323        let now = UnixTime::now();
324        let cipher1 = t.encrypt(b"ticket 1").unwrap();
325        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
326        t.generator = fail_generator;
327        {
328            // Failed new ticketer
329            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
330                now.as_secs() + 10,
331            )));
332        }
333        t.generator = make_ticket_generator;
334        let cipher2 = t.encrypt(b"ticket 2").unwrap();
335        assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
336        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
337        {
338            // recover
339            t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
340                now.as_secs() + 20,
341            )));
342        }
343        let cipher3 = t.encrypt(b"ticket 3").unwrap();
344        assert!(t.decrypt(&cipher1).is_none());
345        assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
346        assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
347    }
348
349    #[test]
350    fn aeadticketer_is_debug_and_producestickets() {
351        use alloc::format;
352
353        use super::*;
354
355        let t = make_ticket_generator().unwrap();
356
357        let expect = format!("AeadTicketer {{ alg: {TICKETER_AEAD:?}, lifetime: 43200 }}");
358        assert_eq!(format!("{t:?}"), expect);
359        assert!(t.enabled());
360        assert_eq!(t.lifetime(), 43200);
361    }
362
363    fn fail_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
364        Err(GetRandomFailed)
365    }
366}