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