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