ant_quic/
token.rs

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