Skip to content

Commit 97e96c7

Browse files
committed
more work
1 parent 70052d3 commit 97e96c7

File tree

14 files changed

+232
-86
lines changed

14 files changed

+232
-86
lines changed

packages/configuration/src/v2_0_0/udp_tracker.rs

+10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
2+
use std::time::Duration;
23

34
use serde::{Deserialize, Serialize};
45

@@ -10,11 +11,16 @@ pub struct UdpTracker {
1011
/// system to choose a random port, use port `0`.
1112
#[serde(default = "UdpTracker::default_bind_address")]
1213
pub bind_address: SocketAddr,
14+
15+
/// The lifetime of the server-generated connection cookie, that is passed
16+
/// the client as the `ConnectionId`.
17+
pub cookie_lifetime: Duration,
1318
}
1419
impl Default for UdpTracker {
1520
fn default() -> Self {
1621
Self {
1722
bind_address: Self::default_bind_address(),
23+
cookie_lifetime: Self::default_cookie_lifetime(),
1824
}
1925
}
2026
}
@@ -23,4 +29,8 @@ impl UdpTracker {
2329
fn default_bind_address() -> SocketAddr {
2430
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969)
2531
}
32+
33+
fn default_cookie_lifetime() -> Duration {
34+
Duration::from_secs(120)
35+
}
2636
}

packages/test-helpers/src/configuration.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Tracker configuration factories for testing.
22
use std::env;
33
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
4+
use std::time::Duration;
45

56
use torrust_tracker_configuration::{Configuration, HttpApi, HttpTracker, Threshold, UdpTracker};
67

@@ -47,6 +48,7 @@ pub fn ephemeral() -> Configuration {
4748
let udp_port = 0u16;
4849
config.udp_trackers = Some(vec![UdpTracker {
4950
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port),
51+
cookie_lifetime: Duration::from_secs(120),
5052
}]);
5153

5254
// Ephemeral socket address for HTTP tracker

src/bootstrap/app.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::bootstrap;
2323
use crate::core::services::tracker_factory;
2424
use crate::core::Tracker;
2525
use crate::shared::crypto::ephemeral_instance_keys;
26-
use crate::shared::crypto::keys::seeds::{self, Keeper as _};
26+
use crate::shared::crypto::keys::{self, Keeper as _};
2727

2828
/// It loads the configuration from the environment and builds the main domain [`Tracker`] struct.
2929
///
@@ -49,11 +49,16 @@ pub fn setup() -> (Configuration, Arc<Tracker>) {
4949
(configuration, tracker)
5050
}
5151

52+
/// checks if the seed is the instance seed in production.
53+
///
54+
/// # Panics
55+
///
56+
/// It would panic if the seed is not the instance seed.
5257
pub fn check_seed() {
53-
let seed = seeds::Current::get_seed();
54-
let instance = seeds::Instance::get_seed();
58+
let seed = keys::Current::get_seed();
59+
let instance = keys::Instance::get_seed();
5560

56-
assert_eq!(seed, instance, "maybe using zeroed see in production!?")
61+
assert_eq!(seed, instance, "maybe using zeroed see in production!?");
5762
}
5863

5964
/// It initializes the application with the given configuration.
@@ -80,6 +85,12 @@ pub fn initialize_static() {
8085

8186
// Initialize the Ephemeral Instance Random Seed
8287
lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED);
88+
89+
// Initialize the Ephemeral Instance Random Cipher
90+
lazy_static::initialize(&ephemeral_instance_keys::RANDOM_CIPHER_BLOWFISH);
91+
92+
// Initialize the Zeroed Cipher
93+
lazy_static::initialize(&ephemeral_instance_keys::ZEROED_TEST_CIPHER_BLOWFISH);
8394
}
8495

8596
/// It builds the domain tracker

src/bootstrap/jobs/udp_tracker.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ use crate::servers::udp::UDP_TRACKER_LOG_TARGET;
3232
#[instrument(skip(config, tracker, form))]
3333
pub async fn start_job(config: &UdpTracker, tracker: Arc<core::Tracker>, form: ServiceRegistrationForm) -> JoinHandle<()> {
3434
let bind_to = config.bind_address;
35+
let cookie_lifetime = config.cookie_lifetime;
3536

3637
let server = Server::new(Spawner::new(bind_to))
37-
.start(tracker, form)
38+
.start(tracker, form, cookie_lifetime)
3839
.await
3940
.expect("it should be able to start the udp tracker");
4041

src/servers/udp/connection_cookie.rs

+54-33
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
//! Module for generating and verifying connection IDs (cookies) used in the UDP tracker protocol.
1+
//! Module for Generating and Verifying Connection IDs (Cookies) in the UDP Tracker Protocol
22
//!
33
//! **Overview:**
44
//!
5-
//! In the BitTorrent UDP tracker protocol, clients must establish a connection by obtaining a connection ID from the server. This connection ID helps prevent IP spoofing and replay attacks by ensuring that only legitimate clients can interact with the tracker.
5+
//! In the `BitTorrent` UDP tracker protocol, clients initiate communication by obtaining a connection ID from the server. This connection ID serves as a safeguard against IP spoofing and replay attacks, ensuring that only legitimate clients can interact with the tracker.
66
//!
7-
//! Instead of storing connection IDs on the server (which would require maintaining state), this module implements a stateless method for generating and verifying connection IDs based on the client's fingerprint (typically derived from the client's IP address) and the time of issuance.
7+
//! To maintain a stateless server architecture, this module implements a method for generating and verifying connection IDs based on the client's fingerprint (typically derived from the client's IP address) and the time of issuance, without storing state on the server.
88
//!
9-
//! The Connection ID is an encrypted opaque cookie that he held by the client. Therefore endianness is not a concern as the same server checks the cookie as produced it.
9+
//! The connection ID is an encrypted, opaque cookie held by the client. Since the same server that generates the cookie also validates it, endianness is not a concern.
1010
//!
1111
//! **Connection ID Generation Algorithm:**
1212
//!
13-
//! The connection ID is generated using the following steps:
14-
//!
1513
//! 1. **Issue Time (`issue_at`):**
1614
//! - Obtain the current time as a 64-bit floating-point number (`f64`), representing seconds since the Unix epoch.
1715
//!
1816
//! 2. **Fingerprint:**
1917
//! - Use an 8-byte fingerprint unique to the client (e.g., derived from the client's IP address).
2018
//!
2119
//! 3. **Assemble Cookie Value:**
22-
//! - Interpret the bytes of `issue_at` as a 64-bit integer (`i64`) without changing the bit pattern.
23-
//! - Interpret the fingerprint bytes as an `i64` in the same way.
24-
//! - Compute `cookie_value = issue_at_i64.wrapping_add(fingerprint_i64)`.
25-
//! - **Note:** Wrapping addition handles potential integer overflows gracefully.
20+
//! - Interpret the bytes of `issue_at` as a 64-bit integer (`i64`) without altering the bit pattern.
21+
//! - Similarly, interpret the fingerprint bytes as an `i64`.
22+
//! - Compute the cookie value:
23+
//! ```rust
24+
//! cookie_value = issue_at_i64.wrapping_add(fingerprint_i64);
25+
//! ```
26+
//! - *Note:* Wrapping addition handles potential integer overflows gracefully.
2627
//!
2728
//! 4. **Encrypt Cookie Value:**
28-
//! - Use a symmetric block cipher (from `Current::get_cipher()`) to encrypt `cookie_value`.
29+
//! - Encrypt `cookie_value` using a symmetric block cipher obtained from `Current::get_cipher()`.
2930
//! - The encrypted `cookie_value` becomes the connection ID sent to the client.
3031
//!
3132
//! **Connection ID Verification Algorithm:**
@@ -34,62 +35,82 @@
3435
//!
3536
//! 1. **Decrypt Connection ID:**
3637
//! - Decrypt the received connection ID using the same cipher to retrieve `cookie_value`.
37-
//! - **Important Note:** The decryption is non-authenticated. This means it does not verify the integrity or authenticity of the ciphertext. The decrypted `cookie_value` can be any byte sequence, including manipulated data.
38+
//! - *Important:* The decryption is non-authenticated, meaning it does not verify the integrity or authenticity of the ciphertext. The decrypted `cookie_value` can be any byte sequence, including manipulated data.
3839
//!
3940
//! 2. **Recover Issue Time:**
4041
//! - Interpret the fingerprint bytes as `i64`.
41-
//! - Compute `issue_at_i64 = cookie_value.wrapping_sub(fingerprint_i64)`.
42-
//! - **Note:** Wrapping subtraction handles potential integer underflows gracefully.
42+
//! - Compute the issue time:
43+
//! ```rust
44+
//! issue_at_i64 = cookie_value.wrapping_sub(fingerprint_i64);
45+
//! ```
46+
//! - *Note:* Wrapping subtraction handles potential integer underflows gracefully.
4347
//! - Reinterpret `issue_at_i64` bytes as an `f64` to get `issue_time`.
4448
//!
4549
//! 3. **Validate Issue Time:**
4650
//! - **Handling Arbitrary `issue_time` Values:**
47-
//! - Since the decrypted `cookie_value` can be arbitrary, `issue_time` can be any `f64` value, including special values like `NaN`, positive or negative infinity, and subnormal numbers.
51+
//! - Since the decrypted `cookie_value` may be arbitrary, `issue_time` can be any `f64` value, including special values like `NaN`, positive or negative infinity, and subnormal numbers.
4852
//! - **Validation Steps:**
4953
//! - **Step 1:** Check if `issue_time` is finite using `issue_time.is_finite()`.
5054
//! - If `issue_time` is `NaN` or infinite, it is considered invalid.
51-
//! - **Step 2:** Perform range checks only if `issue_time` is finite:
52-
//! - Check if `issue_time >= min` and `issue_time <= max`.
55+
//! - **Step 2:** If `issue_time` is finite, perform range checks:
56+
//! - Verify that `min <= issue_time <= max`.
5357
//! - If `issue_time` passes these checks, accept the connection ID; otherwise, reject it with an appropriate error.
5458
//!
5559
//! **Security Considerations:**
5660
//!
5761
//! - **Non-Authenticated Encryption:**
58-
//! - The block size is 8-bytes, there is no good authenticated encryption algorithm that will work with 8-bytes.
59-
//! - It is not an option to expand to 16-bytes, as the protocol is an external and widely adopted deployed standard.
60-
//! - Because the cipher does not provide authentication, attackers could forge or manipulate connection IDs.
61-
//! - However, the probability of an arbitrary 64-bit value decrypting to a valid `issue_time` within the acceptable range is extremely low, and is thus used a form of authentication.
62+
//! - Due to protocol constraints (an 8-byte connection ID), using an authenticated encryption algorithm is not feasible.
63+
//! - As a result, attackers might attempt to forge or manipulate connection IDs.
64+
//! - However, the probability of an arbitrary 64-bit value decrypting to a valid `issue_time` within the acceptable range is extremely low, effectively serving as a form of authentication.
6265
//!
6366
//! - **Handling Special `f64` Values:**
64-
//! - By checking `issue_time.is_finite()`, we prevent `NaN` and infinite values from passing the validation.
65-
//! - This ensures that only valid, finite timestamps are considered.
67+
//! - By checking `issue_time.is_finite()`, the implementation excludes `NaN` and infinite values, ensuring that only valid, finite timestamps are considered.
6668
//!
6769
//! - **Probability of Successful Attack:**
68-
//! - Given the narrow valid time window (usually 2 minutes) compared to the vast range of `f64`, the chance of successfully guessing a valid `issue_time` is negligible.
70+
//! - Given the narrow valid time window (usually around 2 minutes) compared to the vast range of `f64` values, the chance of successfully guessing a valid `issue_time` is negligible.
71+
//!
72+
//! **Key Points:**
73+
//!
74+
//! - The server maintains a stateless design, reducing resource consumption and complexity.
75+
//! - Wrapping arithmetic ensures that the addition and subtraction of `i64` values are safe from overflow or underflow issues.
76+
//! - The validation process is robust against malformed or malicious connection IDs due to stringent checks on the deserialized `issue_time`.
77+
//! - The module leverages existing cryptographic primitives while acknowledging and addressing the limitations imposed by the protocol's specifications.
6978
//!
7079
7180
use aquatic_udp_protocol::ConnectionId as Cookie;
7281
use cookie_builder::{assemble, decode, disassemble, encode};
7382
use zerocopy::AsBytes;
7483

7584
use super::error::{self, Error};
76-
use crate::shared::crypto::keys::CipherArray;
85+
use crate::shared::crypto::keys::CipherArrayBlowfish;
7786

7887
/// Generates a new connection cookie.
88+
///
89+
/// # Panics
90+
///
91+
/// It would panic if the cookie is not exactly 8 bytes is size.
7992
#[must_use]
8093
pub fn make(fingerprint: u64, issue_at: f64) -> Cookie {
8194
let cookie = assemble(fingerprint, issue_at);
8295
let cookie = encode(cookie);
8396

8497
// using `read_from` as the array may be not correctly aligned
85-
zerocopy::FromBytes::read_from(&cookie.as_slice()).expect("it should be the same size")
98+
zerocopy::FromBytes::read_from(cookie.as_slice()).expect("it should be the same size")
8699
}
87100

88101
/// Checks if the supplied `connection_cookie` is valid.
102+
///
103+
/// # Errors
104+
///
105+
/// It would error if the connection cookie is somehow invalid or expired.
106+
///
107+
/// # Panics
108+
///
109+
/// It would panic if somehow the cookie min value is larger than the max value.
89110
pub fn check(cookie: &Cookie, fingerprint: u64, min: f64, max: f64) -> Result<f64, Error> {
90111
assert!(min < max, "invalid constraint on number");
91112

92-
let cookie_bytes = CipherArray::from_slice(cookie.0.as_bytes());
113+
let cookie_bytes = CipherArrayBlowfish::from_slice(cookie.0.as_bytes());
93114
let cookie_bytes = decode(*cookie_bytes);
94115

95116
let issue_time = disassemble(fingerprint, cookie_bytes);
@@ -122,10 +143,10 @@ mod cookie_builder {
122143
use tracing::{instrument, Level};
123144
use zerocopy::{byteorder, AsBytes as _, NativeEndian};
124145

125-
pub type CookiePlainText = CipherArray;
126-
pub type CookieCipherText = CipherArray;
146+
pub type CookiePlainText = CipherArrayBlowfish;
147+
pub type CookieCipherText = CipherArrayBlowfish;
127148

128-
use crate::shared::crypto::keys::{CipherArray, Current, Keeper};
149+
use crate::shared::crypto::keys::{CipherArrayBlowfish, Current, Keeper};
129150

130151
#[instrument(ret(level = Level::TRACE))]
131152
pub(super) fn assemble(fingerprint: u64, issue_at: f64) -> CookiePlainText {
@@ -138,7 +159,7 @@ mod cookie_builder {
138159
let cookie: byteorder::I64<NativeEndian> =
139160
*zerocopy::FromBytes::ref_from(&cookie.to_ne_bytes()).expect("it should be aligned");
140161

141-
*CipherArray::from_slice(cookie.as_bytes())
162+
*CipherArrayBlowfish::from_slice(cookie.as_bytes())
142163
}
143164

144165
#[instrument(ret(level = Level::TRACE))]
@@ -160,7 +181,7 @@ mod cookie_builder {
160181

161182
#[instrument(ret(level = Level::TRACE))]
162183
pub(super) fn encode(mut cookie: CookiePlainText) -> CookieCipherText {
163-
let cipher = Current::get_cipher();
184+
let cipher = Current::get_cipher_blowfish();
164185

165186
cipher.encrypt_block(&mut cookie);
166187

@@ -169,7 +190,7 @@ mod cookie_builder {
169190

170191
#[instrument(ret(level = Level::TRACE))]
171192
pub(super) fn decode(mut cookie: CookieCipherText) -> CookiePlainText {
172-
let cipher = Current::get_cipher();
193+
let cipher = Current::get_cipher_blowfish();
173194

174195
cipher.decrypt_block(&mut cookie);
175196

0 commit comments

Comments
 (0)