noq_proto/
token.rs

1use std::{
2    fmt,
3    net::{IpAddr, SocketAddr},
4};
5
6use bytes::{Buf, BufMut, Bytes};
7use rand::Rng;
8
9use crate::{
10    Duration, RESET_TOKEN_SIZE, ServerConfig, SystemTime, UNIX_EPOCH,
11    coding::{BufExt, BufMutExt},
12    crypto::{HandshakeTokenKey, HmacKey},
13    packet::InitialHeader,
14    shared::ConnectionId,
15};
16
17/// Responsible for limiting clients' ability to reuse validation tokens
18///
19/// [_RFC 9000 § 8.1.4:_](https://www.rfc-editor.org/rfc/rfc9000.html#section-8.1.4)
20///
21/// > Attackers could replay tokens to use servers as amplifiers in DDoS attacks. To protect
22/// > against such attacks, servers MUST ensure that replay of tokens is prevented or limited.
23/// > Servers SHOULD ensure that tokens sent in Retry packets are only accepted for a short time,
24/// > as they are returned immediately by clients. Tokens that are provided in NEW_TOKEN frames
25/// > (Section 19.7) need to be valid for longer but SHOULD NOT be accepted multiple times.
26/// > Servers are encouraged to allow tokens to be used only once, if possible; tokens MAY include
27/// > additional information about clients to further narrow applicability or reuse.
28///
29/// `TokenLog` pertains only to tokens provided in NEW_TOKEN frames.
30pub trait TokenLog: Send + Sync {
31    /// Record that the token was used and, ideally, return a token reuse error if the token may
32    /// have been already used previously
33    ///
34    /// False negatives and false positives are both permissible. Called when a client uses an
35    /// address validation token.
36    ///
37    /// Parameters:
38    /// - `nonce`: A server-generated random unique value for the token.
39    /// - `issued`: The time the server issued the token.
40    /// - `lifetime`: The expiration time of address validation tokens sent via NEW_TOKEN frames,
41    ///   as configured by [`ServerValidationTokenConfig::lifetime`][1].
42    ///
43    /// [1]: crate::ValidationTokenConfig::lifetime
44    ///
45    /// ## Security & Performance
46    ///
47    /// To the extent that it is possible to repeatedly trigger false negatives (returning `Ok` for
48    /// a token which has been reused), an attacker could use the server to perform [amplification
49    /// attacks][2]. The QUIC specification requires that this be limited, if not prevented fully.
50    ///
51    /// A false positive (returning `Err` for a token which has never been used) is not a security
52    /// vulnerability; it is permissible for a `TokenLog` to always return `Err`. A false positive
53    /// causes the token to be ignored, which may cause the transmission of some 0.5-RTT data to be
54    /// delayed until the handshake completes, if a sufficient amount of 0.5-RTT data it sent.
55    ///
56    /// [2]: https://en.wikipedia.org/wiki/Denial-of-service_attack#Amplification
57    fn check_and_insert(
58        &self,
59        nonce: u128,
60        issued: SystemTime,
61        lifetime: Duration,
62    ) -> Result<(), TokenReuseError>;
63}
64
65/// Error for when a validation token may have been reused
66pub struct TokenReuseError;
67
68/// Null implementation of [`TokenLog`], which never accepts tokens
69pub struct NoneTokenLog;
70
71impl TokenLog for NoneTokenLog {
72    fn check_and_insert(&self, _: u128, _: SystemTime, _: Duration) -> Result<(), TokenReuseError> {
73        Err(TokenReuseError)
74    }
75}
76
77/// Responsible for storing validation tokens received from servers and retrieving them for use in
78/// subsequent connections
79pub trait TokenStore: Send + Sync {
80    /// Potentially store a token for later one-time use
81    ///
82    /// Called when a NEW_TOKEN frame is received from the server.
83    fn insert(&self, server_name: &str, token: Bytes);
84
85    /// Try to find and take a token that was stored with the given server name
86    ///
87    /// The same token must never be returned from `take` twice, as doing so can be used to
88    /// de-anonymize a client's traffic.
89    ///
90    /// Called when trying to connect to a server. It is always ok for this to return `None`.
91    fn take(&self, server_name: &str) -> Option<Bytes>;
92}
93
94/// Null implementation of [`TokenStore`], which does not store any tokens
95pub struct NoneTokenStore;
96
97impl TokenStore for NoneTokenStore {
98    fn insert(&self, _: &str, _: Bytes) {}
99    fn take(&self, _: &str) -> Option<Bytes> {
100        None
101    }
102}
103
104/// State in an `Incoming` determined by a token or lack thereof
105#[derive(Debug)]
106pub(crate) struct IncomingToken {
107    pub(crate) retry_src_cid: Option<ConnectionId>,
108    pub(crate) orig_dst_cid: ConnectionId,
109    pub(crate) validated: bool,
110}
111
112impl IncomingToken {
113    /// Construct for an `Incoming` given the first packet header, or error if the connection
114    /// cannot be established
115    pub(crate) fn from_header(
116        header: &InitialHeader,
117        server_config: &ServerConfig,
118        remote_address: SocketAddr,
119    ) -> Result<Self, InvalidRetryTokenError> {
120        let unvalidated = Self {
121            retry_src_cid: None,
122            orig_dst_cid: header.dst_cid,
123            validated: false,
124        };
125
126        // Decode token or short-circuit
127        if header.token.is_empty() {
128            return Ok(unvalidated);
129        }
130
131        // In cases where a token cannot be decrypted/decoded, we must allow for the possibility
132        // that this is caused not by client malfeasance, but by the token having been generated by
133        // an incompatible endpoint, e.g. a different version or a neighbor behind the same load
134        // balancer. In such cases we proceed as if there was no token.
135        //
136        // [_RFC 9000 § 8.1.3:_](https://www.rfc-editor.org/rfc/rfc9000.html#section-8.1.3-10)
137        //
138        // > If the token is invalid, then the server SHOULD proceed as if the client did not have
139        // > a validated address, including potentially sending a Retry packet.
140        let Some(retry) = Token::decode(&*server_config.token_key, &header.token) else {
141            return Ok(unvalidated);
142        };
143
144        // Validate token, then convert into Self
145        match retry.payload {
146            TokenPayload::Retry {
147                address,
148                orig_dst_cid,
149                issued,
150            } => {
151                if address != remote_address {
152                    return Err(InvalidRetryTokenError);
153                }
154                if issued + server_config.retry_token_lifetime < server_config.time_source.now() {
155                    return Err(InvalidRetryTokenError);
156                }
157
158                Ok(Self {
159                    retry_src_cid: Some(header.dst_cid),
160                    orig_dst_cid,
161                    validated: true,
162                })
163            }
164            TokenPayload::Validation { ip, issued } => {
165                if ip != remote_address.ip() {
166                    return Ok(unvalidated);
167                }
168                if issued + server_config.validation_token.lifetime
169                    < server_config.time_source.now()
170                {
171                    return Ok(unvalidated);
172                }
173                if server_config
174                    .validation_token
175                    .log
176                    .check_and_insert(retry.nonce, issued, server_config.validation_token.lifetime)
177                    .is_err()
178                {
179                    return Ok(unvalidated);
180                }
181
182                Ok(Self {
183                    retry_src_cid: None,
184                    orig_dst_cid: header.dst_cid,
185                    validated: true,
186                })
187            }
188        }
189    }
190}
191
192/// Error for a token being unambiguously from a Retry packet, and not valid
193///
194/// The connection cannot be established.
195pub(crate) struct InvalidRetryTokenError;
196
197/// Retry or validation token
198pub(crate) struct Token {
199    /// Content that is encrypted from the client
200    pub(crate) payload: TokenPayload,
201    /// Randomly generated value, which must be unique, and is visible to the client
202    nonce: u128,
203}
204
205impl Token {
206    /// Construct with newly sampled randomness
207    pub(crate) fn new(payload: TokenPayload, rng: &mut impl Rng) -> Self {
208        Self {
209            nonce: rng.random(),
210            payload,
211        }
212    }
213
214    /// Encode and encrypt
215    pub(crate) fn encode(&self, key: &dyn HandshakeTokenKey) -> Vec<u8> {
216        let mut buf = Vec::new();
217
218        // Encode payload
219        match self.payload {
220            TokenPayload::Retry {
221                address,
222                orig_dst_cid,
223                issued,
224            } => {
225                buf.put_u8(TokenType::Retry as u8);
226                encode_addr(&mut buf, address);
227                orig_dst_cid.encode_long(&mut buf);
228                encode_unix_secs(&mut buf, issued);
229            }
230            TokenPayload::Validation { ip, issued } => {
231                buf.put_u8(TokenType::Validation as u8);
232                encode_ip(&mut buf, ip);
233                encode_unix_secs(&mut buf, issued);
234            }
235        }
236
237        // Encrypt
238        key.seal(self.nonce, &mut buf).unwrap();
239        buf.extend(&self.nonce.to_le_bytes());
240
241        buf
242    }
243
244    /// Decode and decrypt
245    fn decode(key: &dyn HandshakeTokenKey, raw_token_bytes: &[u8]) -> Option<Self> {
246        // Decrypt
247
248        let (sealed_token, nonce_bytes) = raw_token_bytes.split_last_chunk()?;
249
250        let nonce = u128::from_le_bytes(*nonce_bytes);
251
252        let mut sealed_token = sealed_token.to_vec();
253        let mut data = key.open(nonce, &mut sealed_token).ok()?;
254
255        // Decode payload
256        let payload = match TokenType::from_byte((&mut data).get::<u8>().ok()?)? {
257            TokenType::Retry => TokenPayload::Retry {
258                address: decode_addr(&mut data)?,
259                orig_dst_cid: ConnectionId::decode_long(&mut data)?,
260                issued: decode_unix_secs(&mut data)?,
261            },
262            TokenType::Validation => TokenPayload::Validation {
263                ip: decode_ip(&mut data)?,
264                issued: decode_unix_secs(&mut data)?,
265            },
266        };
267
268        if !data.is_empty() {
269            // Consider extra bytes a decoding error (it may be from an incompatible endpoint)
270            return None;
271        }
272
273        Some(Self { nonce, payload })
274    }
275}
276
277/// Content of a [`Token`] that is encrypted from the client
278pub(crate) enum TokenPayload {
279    /// Token originating from a Retry packet
280    Retry {
281        /// The client's address
282        address: SocketAddr,
283        /// The destination connection ID set in the very first packet from the client
284        orig_dst_cid: ConnectionId,
285        /// The time at which this token was issued
286        issued: SystemTime,
287    },
288    /// Token originating from a NEW_TOKEN frame
289    Validation {
290        /// The client's IP address (its port is likely to change between sessions)
291        ip: IpAddr,
292        /// The time at which this token was issued
293        issued: SystemTime,
294    },
295}
296
297/// Variant tag for a [`TokenPayload`]
298#[derive(Copy, Clone)]
299#[repr(u8)]
300enum TokenType {
301    Retry = 0,
302    Validation = 1,
303}
304
305impl TokenType {
306    fn from_byte(n: u8) -> Option<Self> {
307        use TokenType::*;
308        [Retry, Validation].into_iter().find(|ty| *ty as u8 == n)
309    }
310}
311
312fn encode_addr(buf: &mut Vec<u8>, address: SocketAddr) {
313    encode_ip(buf, address.ip());
314    buf.put_u16(address.port());
315}
316
317fn decode_addr<B: Buf>(buf: &mut B) -> Option<SocketAddr> {
318    let ip = decode_ip(buf)?;
319    let port = buf.get().ok()?;
320    Some(SocketAddr::new(ip, port))
321}
322
323fn encode_ip(buf: &mut Vec<u8>, ip: IpAddr) {
324    match ip {
325        IpAddr::V4(x) => {
326            buf.put_u8(0);
327            buf.put_slice(&x.octets());
328        }
329        IpAddr::V6(x) => {
330            buf.put_u8(1);
331            buf.put_slice(&x.octets());
332        }
333    }
334}
335
336fn decode_ip<B: Buf>(buf: &mut B) -> Option<IpAddr> {
337    match buf.get::<u8>().ok()? {
338        0 => buf.get().ok().map(IpAddr::V4),
339        1 => buf.get().ok().map(IpAddr::V6),
340        _ => None,
341    }
342}
343
344fn encode_unix_secs(buf: &mut Vec<u8>, time: SystemTime) {
345    buf.write::<u64>(
346        time.duration_since(UNIX_EPOCH)
347            .unwrap_or_default()
348            .as_secs(),
349    );
350}
351
352fn decode_unix_secs<B: Buf>(buf: &mut B) -> Option<SystemTime> {
353    Some(UNIX_EPOCH + Duration::from_secs(buf.get::<u64>().ok()?))
354}
355
356/// Stateless reset token
357///
358/// Used for an endpoint to securely communicate that it has lost state for a connection.
359#[allow(clippy::derived_hash_with_manual_eq)] // Custom PartialEq impl matches derived semantics
360#[derive(Debug, Copy, Clone, Hash)]
361pub(crate) struct ResetToken([u8; RESET_TOKEN_SIZE]);
362
363impl ResetToken {
364    pub(crate) fn new(key: &dyn HmacKey, id: ConnectionId) -> Self {
365        let mut signature = vec![0; key.signature_len()];
366        key.sign(&id, &mut signature);
367        // TODO: Server ID??
368        let mut result = [0; RESET_TOKEN_SIZE];
369        result.copy_from_slice(&signature[..RESET_TOKEN_SIZE]);
370        result.into()
371    }
372}
373
374impl PartialEq for ResetToken {
375    fn eq(&self, other: &Self) -> bool {
376        crate::constant_time::eq(&self.0, &other.0)
377    }
378}
379
380impl Eq for ResetToken {}
381
382impl From<[u8; RESET_TOKEN_SIZE]> for ResetToken {
383    fn from(x: [u8; RESET_TOKEN_SIZE]) -> Self {
384        Self(x)
385    }
386}
387
388impl std::ops::Deref for ResetToken {
389    type Target = [u8];
390    fn deref(&self) -> &[u8] {
391        &self.0
392    }
393}
394
395impl fmt::Display for ResetToken {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        for byte in self.iter() {
398            write!(f, "{byte:02x}")?;
399        }
400        Ok(())
401    }
402}
403
404#[cfg(all(test, any(feature = "aws-lc-rs", feature = "ring")))]
405mod test {
406    use crate::crypto::ring_like::RetryTokenKey;
407
408    use super::*;
409    use rand::prelude::*;
410
411    fn token_round_trip(payload: TokenPayload) -> TokenPayload {
412        let rng = &mut rand::rng();
413        let token = Token::new(payload, rng);
414        let master_key = RetryTokenKey::new(rng);
415        let encoded = token.encode(&master_key);
416        let decoded = Token::decode(&master_key, &encoded).expect("token didn't decrypt / decode");
417        assert_eq!(token.nonce, decoded.nonce);
418        decoded.payload
419    }
420
421    #[test]
422    fn retry_token_sanity() {
423        use crate::MAX_CID_SIZE;
424        use crate::cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator};
425        use crate::{Duration, UNIX_EPOCH};
426
427        use std::net::Ipv6Addr;
428
429        let address_1 = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433);
430        let orig_dst_cid_1 = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid();
431        let issued_1 = UNIX_EPOCH + Duration::from_secs(42); // Fractional seconds would be lost
432        let payload_1 = TokenPayload::Retry {
433            address: address_1,
434            orig_dst_cid: orig_dst_cid_1,
435            issued: issued_1,
436        };
437        let TokenPayload::Retry {
438            address: address_2,
439            orig_dst_cid: orig_dst_cid_2,
440            issued: issued_2,
441        } = token_round_trip(payload_1)
442        else {
443            panic!("token decoded as wrong variant");
444        };
445
446        assert_eq!(address_1, address_2);
447        assert_eq!(orig_dst_cid_1, orig_dst_cid_2);
448        assert_eq!(issued_1, issued_2);
449    }
450
451    #[test]
452    fn validation_token_sanity() {
453        use crate::{Duration, UNIX_EPOCH};
454
455        use std::net::Ipv6Addr;
456
457        let ip_1 = Ipv6Addr::LOCALHOST.into();
458        let issued_1 = UNIX_EPOCH + Duration::from_secs(42); // Fractional seconds would be lost
459
460        let payload_1 = TokenPayload::Validation {
461            ip: ip_1,
462            issued: issued_1,
463        };
464        let TokenPayload::Validation {
465            ip: ip_2,
466            issued: issued_2,
467        } = token_round_trip(payload_1)
468        else {
469            panic!("token decoded as wrong variant");
470        };
471
472        assert_eq!(ip_1, ip_2);
473        assert_eq!(issued_1, issued_2);
474    }
475
476    #[test]
477    fn invalid_token_returns_err() {
478        use super::*;
479
480        let master_key = RetryTokenKey::new(&mut rand::rng());
481
482        let mut invalid_token = Vec::new();
483
484        let mut random_data = [0; 32];
485        rand::rng().fill_bytes(&mut random_data);
486        invalid_token.put_slice(&random_data);
487
488        // Assert: garbage sealed data returns err
489        assert!(Token::decode(&master_key, &invalid_token).is_none());
490    }
491}