Skip to content

Commit 350e0f2

Browse files
Implement ticketer
1 parent dfb0728 commit 350e0f2

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ logging = ["rustls/logging"]
8484
tls12 = ["rustls/tls12"]
8585

8686
# RustCrypto is preparing to migrate to core::error::Error
87-
# and in before most of the use case for std is just std::error::Error
87+
# and in before most of the use case for std is just std::error::Error
8888
std = ["alloc", "rustls/std", "ed448-goldilocks?/std", "tinyvec?/std"]
8989
alloc = [
9090
"ecdsa?/alloc",
@@ -222,6 +222,7 @@ hash-sha512 = ["hash"]
222222
hash-full = ["hash-sha224", "hash-sha256", "hash-sha384", "hash-sha512"]
223223

224224
quic = ["aead", "chacha20?/cipher", "tinyvec"]
225+
ticketer = ["aead", "chacha20poly1305", "rand"]
225226

226227
# Formats
227228
der = ["dep:der", "sec1?/der"]

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ pub mod verify;
108108
#[cfg(feature = "quic")]
109109
pub mod quic;
110110

111+
#[cfg(feature = "ticketer")]
112+
pub mod ticketer;
113+
111114
const _: () = assert!(
112115
!ALL_CIPHER_SUITES.is_empty(),
113116
"At least one cipher suite should be enabled"

src/ticketer.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#[cfg(feature = "alloc")]
2+
use alloc::boxed::Box;
3+
#[cfg(feature = "alloc")]
4+
use alloc::sync::Arc;
5+
#[cfg(feature = "alloc")]
6+
use alloc::vec::Vec;
7+
use core::fmt::{Debug, Formatter};
8+
use core::sync::atomic::{AtomicUsize, Ordering};
9+
use core::{fmt, time};
10+
11+
use aead::{AeadInOut, KeyInit};
12+
use elliptic_curve::subtle::ConstantTimeEq;
13+
use rand_core::{OsRng, TryRngCore};
14+
use rustls::crypto::GetRandomFailed;
15+
use rustls::server::ProducesTickets;
16+
use rustls::{Error, ticketer::TicketRotator};
17+
18+
#[cfg(feature = "chacha20poly1305")]
19+
use chacha20poly1305::ChaCha20Poly1305;
20+
21+
fn try_split_at(data: &[u8], at: usize) -> Option<(&[u8], &[u8])> {
22+
if data.len() < at {
23+
None
24+
} else {
25+
Some(data.split_at(at))
26+
}
27+
}
28+
29+
/// A concrete, safe ticket creation mechanism.
30+
#[non_exhaustive]
31+
pub struct Ticketer {}
32+
33+
impl Ticketer {
34+
/// Make the recommended `Ticketer`.
35+
///
36+
/// This produces tickets:
37+
///
38+
/// - where each lasts for at least 6 hours,
39+
/// - with randomly generated keys, and
40+
/// - where keys are rotated every 6 hours.
41+
///
42+
/// The encryption mechanism used is Chacha20Poly1305.
43+
44+
pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
45+
Ok(Arc::new(TicketRotator::new(
46+
time::Duration::from_hours(6).as_secs() as u32,
47+
|| Ok(Box::new(AeadTicketProducer::new()?)),
48+
)?))
49+
}
50+
}
51+
52+
/// This is a `ProducesTickets` implementation which uses
53+
/// ChaCha20Poly1305 to encrypt and authenticate
54+
/// the ticket payload. It does not enforce any lifetime
55+
/// constraint.
56+
struct AeadTicketProducer {
57+
key: ChaCha20Poly1305,
58+
key_name: [u8; 16],
59+
60+
/// Tracks the largest ciphertext produced by `encrypt`, and
61+
/// uses it to early-reject `decrypt` queries that are too long.
62+
///
63+
/// Accepting excessively long ciphertexts means a "Partitioning
64+
/// Oracle Attack" (see <https://eprint.iacr.org/2020/1491.pdf>)
65+
/// can be more efficient, though also note that these are thought
66+
/// to be cryptographically hard if the key is full-entropy (as it
67+
/// is here).
68+
maximum_ciphertext_len: AtomicUsize,
69+
}
70+
71+
impl AeadTicketProducer {
72+
fn new() -> Result<Self, GetRandomFailed> {
73+
let mut key_bytes = [0u8; 32];
74+
OsRng
75+
.try_fill_bytes(&mut key_bytes)
76+
.map_err(|_| GetRandomFailed)?;
77+
78+
let key = ChaCha20Poly1305::new_from_slice(&key_bytes).map_err(|_| GetRandomFailed)?;
79+
80+
let mut key_name = [0u8; 16];
81+
OsRng
82+
.try_fill_bytes(&mut key_name)
83+
.map_err(|_| GetRandomFailed)?;
84+
85+
Ok(Self {
86+
key,
87+
key_name,
88+
maximum_ciphertext_len: AtomicUsize::new(0),
89+
})
90+
}
91+
}
92+
93+
impl ProducesTickets for AeadTicketProducer {
94+
fn enabled(&self) -> bool {
95+
true
96+
}
97+
98+
fn lifetime(&self) -> u32 {
99+
// this is not used, as this ticketer is only used via a `TicketRotator`
100+
// that is responsible for defining and managing the lifetime of tickets.
101+
0
102+
}
103+
104+
/// Encrypt `message` and return the ciphertext.
105+
fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
106+
// Random nonce, because a counter is a privacy leak.
107+
let mut nonce_buf = [0u8; 12];
108+
OsRng.try_fill_bytes(&mut nonce_buf).ok()?;
109+
let nonce = nonce_buf.try_into().ok()?;
110+
111+
// ciphertext structure is:
112+
// key_name: [u8; 16]
113+
// nonce: [u8; 12]
114+
// message: [u8, _]
115+
// tag: [u8; 16]
116+
117+
let mut ciphertext =
118+
Vec::with_capacity(self.key_name.len() + nonce_buf.len() + message.len() + 16);
119+
ciphertext.extend(self.key_name);
120+
ciphertext.extend(nonce_buf);
121+
ciphertext.extend(message);
122+
let tag = self
123+
.key
124+
.encrypt_inout_detached(
125+
&nonce,
126+
&self.key_name,
127+
(&mut ciphertext[self.key_name.len() + nonce_buf.len()..]).into(),
128+
)
129+
.ok()?;
130+
ciphertext.extend(tag);
131+
132+
self.maximum_ciphertext_len
133+
.fetch_max(ciphertext.len(), Ordering::SeqCst);
134+
Some(ciphertext)
135+
}
136+
137+
/// Decrypt `ciphertext` and recover the original message.
138+
fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
139+
if ciphertext.len() > self.maximum_ciphertext_len.load(Ordering::SeqCst) {
140+
return None;
141+
}
142+
143+
let (alleged_key_name, ciphertext) = try_split_at(ciphertext, self.key_name.len())?;
144+
145+
let (nonce_bytes, ciphertext) = try_split_at(ciphertext, 12)?;
146+
147+
// checking the key_name is the expected one, *and* then putting it into the
148+
// additionally authenticated data is duplicative. this check quickly rejects
149+
// tickets for a different ticketer (see `TicketRotator`), while including it
150+
// in the AAD ensures it is authenticated independent of that check and that
151+
// any attempted attack on the integrity such as [^1] must happen for each
152+
// `key_label`, not over a population of potential keys. this approach
153+
// is overall similar to [^2].
154+
//
155+
// [^1]: https://eprint.iacr.org/2020/1491.pdf
156+
// [^2]: "Authenticated Encryption with Key Identification", fig 6
157+
// <https://eprint.iacr.org/2022/1680.pdf>
158+
if ConstantTimeEq::ct_ne(&self.key_name[..], alleged_key_name).into() {
159+
return None;
160+
}
161+
162+
let nonce = nonce_bytes.try_into().ok()?;
163+
164+
let mut out = Vec::from(ciphertext);
165+
let tag_vec = out.split_off(out.len() - 16);
166+
let tag = tag_vec.try_into().ok()?;
167+
168+
self.key
169+
.decrypt_inout_detached(&nonce, alleged_key_name, (&mut out[..]).into(), &tag)
170+
.ok()?;
171+
let plain_len = out.len();
172+
out.truncate(plain_len);
173+
174+
Some(out)
175+
}
176+
}
177+
178+
impl Debug for AeadTicketProducer {
179+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
180+
// Note: we deliberately omit the key from the debug output.
181+
f.debug_struct("AeadTicketer").finish()
182+
}
183+
}

0 commit comments

Comments
 (0)