rustls/crypto/ring/
ticketer.rs

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