mirror of https://github.com/telemt/telemt.git
refactor: update TLS record size constants and related validations
- Rename MAX_TLS_RECORD_SIZE to MAX_TLS_PLAINTEXT_SIZE for clarity. - Rename MAX_TLS_CHUNK_SIZE to MAX_TLS_CIPHERTEXT_SIZE to reflect its purpose. - Deprecate old constants in favor of new ones. - Update various parts of the codebase to use the new constants, including validation checks and tests. - Add new tests to ensure compliance with RFC 8446 regarding TLS record sizes.
This commit is contained in:
parent
801f670827
commit
3abde52de8
|
|
@ -152,17 +152,29 @@ pub const TLS_RECORD_CHANGE_CIPHER: u8 = 0x14;
|
||||||
pub const TLS_RECORD_APPLICATION: u8 = 0x17;
|
pub const TLS_RECORD_APPLICATION: u8 = 0x17;
|
||||||
/// TLS record type: Alert
|
/// TLS record type: Alert
|
||||||
pub const TLS_RECORD_ALERT: u8 = 0x15;
|
pub const TLS_RECORD_ALERT: u8 = 0x15;
|
||||||
/// Maximum TLS record size (RFC 8446 §5.1: MUST NOT exceed 2^14 = 16_384 bytes)
|
/// Maximum TLS plaintext record payload size.
|
||||||
pub const MAX_TLS_RECORD_SIZE: usize = 16_384;
|
/// RFC 8446 §5.1: "The length MUST NOT exceed 2^14 bytes."
|
||||||
|
/// Use this for validating incoming unencrypted records
|
||||||
|
/// (ClientHello, ChangeCipherSpec, unprotected Handshake messages).
|
||||||
|
pub const MAX_TLS_PLAINTEXT_SIZE: usize = 16_384;
|
||||||
|
|
||||||
/// Structural minimum for a valid TLS 1.3 ClientHello with SNI.
|
/// Structural minimum for a valid TLS 1.3 ClientHello with SNI.
|
||||||
/// Derived from RFC 8446 §4.1.2 field layout + Appendix D.4 compat mode.
|
/// Derived from RFC 8446 §4.1.2 field layout + Appendix D.4 compat mode.
|
||||||
/// Deliberately conservative (below any real client) to avoid false
|
/// Deliberately conservative (below any real client) to avoid false
|
||||||
/// positives on legitimate connections with compact extension sets.
|
/// positives on legitimate connections with compact extension sets.
|
||||||
pub const MIN_TLS_CLIENT_HELLO_SIZE: usize = 100;
|
pub const MIN_TLS_CLIENT_HELLO_SIZE: usize = 100;
|
||||||
/// Maximum TLS chunk size (with overhead)
|
|
||||||
/// RFC 8446 §5.2 allows up to 16384 + 256 bytes of ciphertext
|
/// Maximum TLS ciphertext record payload size.
|
||||||
pub const MAX_TLS_CHUNK_SIZE: usize = 16384 + 256;
|
/// RFC 8446 §5.2: "The length MUST NOT exceed 2^14 + 256 bytes."
|
||||||
|
/// The +256 accounts for maximum AEAD expansion overhead.
|
||||||
|
/// Use this for validating or sizing buffers for encrypted records.
|
||||||
|
pub const MAX_TLS_CIPHERTEXT_SIZE: usize = 16_384 + 256;
|
||||||
|
|
||||||
|
#[deprecated(note = "use MAX_TLS_PLAINTEXT_SIZE")]
|
||||||
|
pub const MAX_TLS_RECORD_SIZE: usize = MAX_TLS_PLAINTEXT_SIZE;
|
||||||
|
|
||||||
|
#[deprecated(note = "use MAX_TLS_CIPHERTEXT_SIZE")]
|
||||||
|
pub const MAX_TLS_CHUNK_SIZE: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
|
|
||||||
/// Secure Intermediate payload is expected to be 4-byte aligned.
|
/// Secure Intermediate payload is expected to be 4-byte aligned.
|
||||||
pub fn is_valid_secure_payload_len(data_len: usize) -> bool {
|
pub fn is_valid_secure_payload_len(data_len: usize) -> bool {
|
||||||
|
|
@ -325,6 +337,10 @@ pub mod rpc_flags {
|
||||||
|
|
||||||
pub const ME_CONNECT_TIMEOUT_SECS: u64 = 5;
|
pub const ME_CONNECT_TIMEOUT_SECS: u64 = 5;
|
||||||
pub const ME_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
|
pub const ME_HANDSHAKE_TIMEOUT_SECS: u64 = 10;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tls_size_constants_security_tests.rs"]
|
||||||
|
mod tls_size_constants_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@ pub fn build_server_hello(
|
||||||
new_session_tickets: u8,
|
new_session_tickets: u8,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
const MIN_APP_DATA: usize = 64;
|
const MIN_APP_DATA: usize = 64;
|
||||||
const MAX_APP_DATA: usize = 16640; // RFC 8446 §5.2 upper bound
|
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
let fake_cert_len = fake_cert_len.clamp(MIN_APP_DATA, MAX_APP_DATA);
|
let fake_cert_len = fake_cert_len.clamp(MIN_APP_DATA, MAX_APP_DATA);
|
||||||
let x25519_key = gen_fake_x25519_key(rng);
|
let x25519_key = gen_fake_x25519_key(rng);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1189,7 +1189,7 @@ fn test_parse_tls_record_header() {
|
||||||
let header = [0x17, 0x03, 0x03, 0x40, 0x00];
|
let header = [0x17, 0x03, 0x03, 0x40, 0x00];
|
||||||
let result = parse_tls_record_header(&header).unwrap();
|
let result = parse_tls_record_header(&header).unwrap();
|
||||||
assert_eq!(result.0, TLS_RECORD_APPLICATION);
|
assert_eq!(result.0, TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(result.1, 16384);
|
assert_eq!(usize::from(result.1), MAX_TLS_PLAINTEXT_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1887,7 +1887,7 @@ fn server_hello_clamps_fake_cert_len_upper_bound() {
|
||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
|
|
||||||
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(app_len, 16_640, "fake cert payload must be clamped to TLS record max bound");
|
assert_eq!(app_len, MAX_TLS_CIPHERTEXT_SIZE, "fake cert payload must be clamped to TLS record max bound");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
use super::{
|
||||||
|
MAX_TLS_CIPHERTEXT_SIZE,
|
||||||
|
MAX_TLS_PLAINTEXT_SIZE,
|
||||||
|
MIN_TLS_CLIENT_HELLO_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_size_constants_match_rfc_8446() {
|
||||||
|
assert_eq!(MAX_TLS_PLAINTEXT_SIZE, 16_384);
|
||||||
|
assert_eq!(MAX_TLS_CIPHERTEXT_SIZE, 16_640);
|
||||||
|
|
||||||
|
assert!(MIN_TLS_CLIENT_HELLO_SIZE < 512);
|
||||||
|
assert!(MIN_TLS_CLIENT_HELLO_SIZE > 64);
|
||||||
|
assert!(MAX_TLS_CIPHERTEXT_SIZE > MAX_TLS_PLAINTEXT_SIZE);
|
||||||
|
}
|
||||||
|
|
@ -111,7 +111,7 @@ fn wrap_tls_application_record(payload: &[u8]) -> Vec<u8> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tls_clienthello_len_in_bounds(tls_len: usize) -> bool {
|
fn tls_clienthello_len_in_bounds(tls_len: usize) -> bool {
|
||||||
(MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_RECORD_SIZE).contains(&tls_len)
|
(MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_PLAINTEXT_SIZE).contains(&tls_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_with_progress<R: AsyncRead + Unpin>(reader: &mut R, mut buf: &mut [u8]) -> std::io::Result<usize> {
|
async fn read_with_progress<R: AsyncRead + Unpin>(reader: &mut R, mut buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
|
@ -281,7 +281,7 @@ where
|
||||||
// incorrectly rejecting compact but spec-compliant ClientHellos from
|
// incorrectly rejecting compact but spec-compliant ClientHellos from
|
||||||
// third-party clients or future Telegram versions.
|
// third-party clients or future Telegram versions.
|
||||||
if !tls_clienthello_len_in_bounds(tls_len) {
|
if !tls_clienthello_len_in_bounds(tls_len) {
|
||||||
debug!(peer = %real_peer, tls_len = tls_len, max_tls_len = MAX_TLS_RECORD_SIZE, "TLS handshake length out of bounds");
|
debug!(peer = %real_peer, tls_len = tls_len, max_tls_len = MAX_TLS_PLAINTEXT_SIZE, "TLS handshake length out of bounds");
|
||||||
stats.increment_connects_bad();
|
stats.increment_connects_bad();
|
||||||
let (reader, writer) = tokio::io::split(stream);
|
let (reader, writer) = tokio::io::split(stream);
|
||||||
handle_bad_client(
|
handle_bad_client(
|
||||||
|
|
@ -729,7 +729,7 @@ impl RunningClientHandler {
|
||||||
// incorrectly rejecting compact but spec-compliant ClientHellos from
|
// incorrectly rejecting compact but spec-compliant ClientHellos from
|
||||||
// third-party clients or future Telegram versions.
|
// third-party clients or future Telegram versions.
|
||||||
if !tls_clienthello_len_in_bounds(tls_len) {
|
if !tls_clienthello_len_in_bounds(tls_len) {
|
||||||
debug!(peer = %peer, tls_len = tls_len, max_tls_len = MAX_TLS_RECORD_SIZE, "TLS handshake length out of bounds");
|
debug!(peer = %peer, tls_len = tls_len, max_tls_len = MAX_TLS_PLAINTEXT_SIZE, "TLS handshake length out of bounds");
|
||||||
self.stats.increment_connects_bad();
|
self.stats.increment_connects_bad();
|
||||||
let (reader, writer) = self.stream.into_split();
|
let (reader, writer) = self.stream.into_split();
|
||||||
handle_bad_client(
|
handle_bad_client(
|
||||||
|
|
|
||||||
|
|
@ -3335,8 +3335,8 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() {
|
||||||
0x16,
|
0x16,
|
||||||
0x03,
|
0x03,
|
||||||
0x01,
|
0x01,
|
||||||
(((MAX_TLS_RECORD_SIZE + 1) >> 8) & 0xff) as u8,
|
(((MAX_TLS_PLAINTEXT_SIZE + 1) >> 8) & 0xff) as u8,
|
||||||
((MAX_TLS_RECORD_SIZE + 1) & 0xff) as u8,
|
((MAX_TLS_PLAINTEXT_SIZE + 1) & 0xff) as u8,
|
||||||
];
|
];
|
||||||
let backend_reply = b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n".to_vec();
|
let backend_reply = b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n".to_vec();
|
||||||
|
|
||||||
|
|
@ -3438,8 +3438,8 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() {
|
||||||
0x16,
|
0x16,
|
||||||
0x03,
|
0x03,
|
||||||
0x01,
|
0x01,
|
||||||
(((MAX_TLS_RECORD_SIZE + 1) >> 8) & 0xff) as u8,
|
(((MAX_TLS_PLAINTEXT_SIZE + 1) >> 8) & 0xff) as u8,
|
||||||
((MAX_TLS_RECORD_SIZE + 1) & 0xff) as u8,
|
((MAX_TLS_PLAINTEXT_SIZE + 1) & 0xff) as u8,
|
||||||
];
|
];
|
||||||
let backend_reply = b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n".to_vec();
|
let backend_reply = b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n".to_vec();
|
||||||
|
|
||||||
|
|
@ -3769,7 +3769,7 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() {
|
||||||
let backend_addr = listener.local_addr().unwrap();
|
let backend_addr = listener.local_addr().unwrap();
|
||||||
|
|
||||||
let secret = [0x55u8; 16];
|
let secret = [0x55u8; 16];
|
||||||
let client_hello = make_valid_tls_client_hello_with_len(&secret, 0, MAX_TLS_RECORD_SIZE);
|
let client_hello = make_valid_tls_client_hello_with_len(&secret, 0, MAX_TLS_PLAINTEXT_SIZE);
|
||||||
|
|
||||||
let mut cfg = ProxyConfig::default();
|
let mut cfg = ProxyConfig::default();
|
||||||
cfg.general.beobachten = false;
|
cfg.general.beobachten = false;
|
||||||
|
|
@ -3865,7 +3865,7 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() {
|
||||||
let front_addr = front_listener.local_addr().unwrap();
|
let front_addr = front_listener.local_addr().unwrap();
|
||||||
|
|
||||||
let secret = [0x66u8; 16];
|
let secret = [0x66u8; 16];
|
||||||
let client_hello = make_valid_tls_client_hello_with_len(&secret, 0, MAX_TLS_RECORD_SIZE);
|
let client_hello = make_valid_tls_client_hello_with_len(&secret, 0, MAX_TLS_PLAINTEXT_SIZE);
|
||||||
|
|
||||||
let mut cfg = ProxyConfig::default();
|
let mut cfg = ProxyConfig::default();
|
||||||
cfg.general.beobachten = false;
|
cfg.general.beobachten = false;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use crate::protocol::constants::{MAX_TLS_RECORD_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
use crate::protocol::constants::{MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
@ -124,7 +124,7 @@ async fn tls_client_hello_lower_bound_minus_one_is_masked_and_counted_bad() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn tls_client_hello_upper_bound_plus_one_is_masked_and_counted_bad() {
|
async fn tls_client_hello_upper_bound_plus_one_is_masked_and_counted_bad() {
|
||||||
run_probe_and_assert_masking(MAX_TLS_RECORD_SIZE + 1, true).await;
|
run_probe_and_assert_masking(MAX_TLS_PLAINTEXT_SIZE + 1, true).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -142,9 +142,9 @@ fn tls_client_hello_len_bounds_unit_adversarial_sweep() {
|
||||||
(101usize, true),
|
(101usize, true),
|
||||||
(511usize, true),
|
(511usize, true),
|
||||||
(512usize, true),
|
(512usize, true),
|
||||||
(16_383usize, true),
|
(MAX_TLS_PLAINTEXT_SIZE - 1, true),
|
||||||
(16_384usize, true),
|
(MAX_TLS_PLAINTEXT_SIZE, true),
|
||||||
(16_385usize, false),
|
(MAX_TLS_PLAINTEXT_SIZE + 1, false),
|
||||||
(u16::MAX as usize, false),
|
(u16::MAX as usize, false),
|
||||||
(usize::MAX, false),
|
(usize::MAX, false),
|
||||||
];
|
];
|
||||||
|
|
@ -168,12 +168,12 @@ fn tls_client_hello_len_bounds_light_fuzz_deterministic_lcg() {
|
||||||
0 => MIN_TLS_CLIENT_HELLO_SIZE - 1,
|
0 => MIN_TLS_CLIENT_HELLO_SIZE - 1,
|
||||||
1 => MIN_TLS_CLIENT_HELLO_SIZE,
|
1 => MIN_TLS_CLIENT_HELLO_SIZE,
|
||||||
2 => MIN_TLS_CLIENT_HELLO_SIZE + 1,
|
2 => MIN_TLS_CLIENT_HELLO_SIZE + 1,
|
||||||
3 => MAX_TLS_RECORD_SIZE - 1,
|
3 => MAX_TLS_PLAINTEXT_SIZE - 1,
|
||||||
4 => MAX_TLS_RECORD_SIZE,
|
4 => MAX_TLS_PLAINTEXT_SIZE,
|
||||||
5 => MAX_TLS_RECORD_SIZE + 1,
|
5 => MAX_TLS_PLAINTEXT_SIZE + 1,
|
||||||
_ => base,
|
_ => base,
|
||||||
};
|
};
|
||||||
let expect_bad = !(MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_RECORD_SIZE).contains(&len);
|
let expect_bad = !(MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_PLAINTEXT_SIZE).contains(&len);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tls_clienthello_len_in_bounds(len),
|
tls_clienthello_len_in_bounds(len),
|
||||||
!expect_bad,
|
!expect_bad,
|
||||||
|
|
@ -186,9 +186,9 @@ fn tls_client_hello_len_bounds_light_fuzz_deterministic_lcg() {
|
||||||
fn tls_client_hello_len_bounds_stress_many_evaluations() {
|
fn tls_client_hello_len_bounds_stress_many_evaluations() {
|
||||||
for _ in 0..100_000 {
|
for _ in 0..100_000 {
|
||||||
assert!(tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE));
|
assert!(tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE));
|
||||||
assert!(tls_clienthello_len_in_bounds(MAX_TLS_RECORD_SIZE));
|
assert!(tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE));
|
||||||
assert!(!tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE - 1));
|
assert!(!tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE - 1));
|
||||||
assert!(!tls_clienthello_len_in_bounds(MAX_TLS_RECORD_SIZE + 1));
|
assert!(!tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, MAX_TLS_CHUNK_SIZE, TLS_VERSION};
|
use crate::protocol::constants::{HANDSHAKE_LEN, MAX_TLS_CIPHERTEXT_SIZE, TLS_VERSION};
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
@ -347,7 +347,7 @@ async fn tls_bad_mtproto_fallback_forwards_max_tls_record_verbatim() {
|
||||||
let client_hello = make_valid_tls_client_hello(&secret, 3, 600, 0x45);
|
let client_hello = make_valid_tls_client_hello(&secret, 3, 600, 0x45);
|
||||||
let invalid_mtproto = vec![0u8; HANDSHAKE_LEN];
|
let invalid_mtproto = vec![0u8; HANDSHAKE_LEN];
|
||||||
let invalid_mtproto_record = wrap_tls_application_data(&invalid_mtproto);
|
let invalid_mtproto_record = wrap_tls_application_data(&invalid_mtproto);
|
||||||
let trailing_payload = vec![0xAB; MAX_TLS_CHUNK_SIZE];
|
let trailing_payload = vec![0xAB; MAX_TLS_CIPHERTEXT_SIZE];
|
||||||
let trailing_record = wrap_tls_application_data(&trailing_payload);
|
let trailing_record = wrap_tls_application_data(&trailing_payload);
|
||||||
|
|
||||||
let expected_client_hello = client_hello.clone();
|
let expected_client_hello = client_hello.clone();
|
||||||
|
|
@ -1786,7 +1786,7 @@ async fn tls_bad_mtproto_fallback_coalesced_tail_max_payload_is_forwarded() {
|
||||||
|
|
||||||
let secret = [0xA5u8; 16];
|
let secret = [0xA5u8; 16];
|
||||||
let client_hello = make_valid_tls_client_hello(&secret, 304, 600, 0x35);
|
let client_hello = make_valid_tls_client_hello(&secret, 304, 600, 0x35);
|
||||||
let coalesced_tail = vec![0xEF; MAX_TLS_CHUNK_SIZE - HANDSHAKE_LEN];
|
let coalesced_tail = vec![0xEF; MAX_TLS_CIPHERTEXT_SIZE - HANDSHAKE_LEN];
|
||||||
let coalesced_record = wrap_invalid_mtproto_with_coalesced_tail(&coalesced_tail);
|
let coalesced_record = wrap_invalid_mtproto_with_coalesced_tail(&coalesced_tail);
|
||||||
let expected_tail_record = wrap_tls_application_data(&coalesced_tail);
|
let expected_tail_record = wrap_tls_application_data(&coalesced_tail);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
//! Telegram MTProto proxy "FakeTLS" mode uses a TLS-looking outer layer for
|
//! Telegram MTProto proxy "FakeTLS" mode uses a TLS-looking outer layer for
|
||||||
//! domain fronting / traffic camouflage. iOS Telegram clients are known to
|
//! domain fronting / traffic camouflage. iOS Telegram clients are known to
|
||||||
//! produce slightly different TLS record sizing patterns than Android/Desktop,
|
//! produce slightly different TLS record sizing patterns than Android/Desktop,
|
||||||
//! including records that exceed 16384 payload bytes by a small overhead.
|
//! including records that exceed MAX_TLS_PLAINTEXT_SIZE payload bytes by a small overhead.
|
||||||
//!
|
//!
|
||||||
//! Key design principles:
|
//! Key design principles:
|
||||||
//! - Explicit state machines for all async operations
|
//! - Explicit state machines for all async operations
|
||||||
|
|
@ -23,14 +23,13 @@
|
||||||
//! - Proper handling of all TLS record types
|
//! - Proper handling of all TLS record types
|
||||||
//!
|
//!
|
||||||
//! Important nuance (Telegram FakeTLS):
|
//! Important nuance (Telegram FakeTLS):
|
||||||
//! - The TLS spec limits "plaintext fragments" to 2^14 (16384) bytes.
|
//! - The TLS spec limits "plaintext fragments" to MAX_TLS_PLAINTEXT_SIZE bytes.
|
||||||
//! - However, the on-the-wire record length can exceed 16384 because TLS 1.3
|
//! - However, the on-the-wire record length can exceed MAX_TLS_PLAINTEXT_SIZE because TLS 1.3
|
||||||
//! uses AEAD and can include tag/overhead/padding.
|
//! uses AEAD and can include tag/overhead/padding.
|
||||||
//! - Telegram FakeTLS clients (notably iOS) may send Application Data records
|
//! - Telegram FakeTLS clients (notably iOS) may send Application Data records
|
||||||
//! with length up to 16384 + 256 bytes (RFC 8446 §5.2). We accept that as
|
//! with length up to MAX_TLS_CIPHERTEXT_SIZE bytes (RFC 8446 §5.2).
|
||||||
//! MAX_TLS_CHUNK_SIZE.
|
|
||||||
//!
|
//!
|
||||||
//! If you reject those (e.g. validate length <= 16384), you will see errors like:
|
//! If you reject those (e.g. validate length <= MAX_TLS_PLAINTEXT_SIZE), you will see errors like:
|
||||||
//! "TLS record too large: 16408 bytes"
|
//! "TLS record too large: 16408 bytes"
|
||||||
//! and uploads from iOS will break (media/file sending), while small traffic
|
//! and uploads from iOS will break (media/file sending), while small traffic
|
||||||
//! may still work.
|
//! may still work.
|
||||||
|
|
@ -42,10 +41,11 @@ use std::task::{Context, Poll};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt, ReadBuf};
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt, ReadBuf};
|
||||||
|
|
||||||
use crate::protocol::constants::{
|
use crate::protocol::constants::{
|
||||||
|
MAX_TLS_PLAINTEXT_SIZE,
|
||||||
TLS_VERSION,
|
TLS_VERSION,
|
||||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER,
|
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER,
|
||||||
TLS_RECORD_HANDSHAKE, TLS_RECORD_ALERT,
|
TLS_RECORD_HANDSHAKE, TLS_RECORD_ALERT,
|
||||||
MAX_TLS_CHUNK_SIZE,
|
MAX_TLS_CIPHERTEXT_SIZE,
|
||||||
};
|
};
|
||||||
use super::state::{StreamState, HeaderBuffer, YieldBuffer, WriteBuffer};
|
use super::state::{StreamState, HeaderBuffer, YieldBuffer, WriteBuffer};
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ const TLS_HEADER_SIZE: usize = 5;
|
||||||
|
|
||||||
/// Maximum TLS fragment size we emit for Application Data.
|
/// Maximum TLS fragment size we emit for Application Data.
|
||||||
/// Real TLS 1.3 allows up to 16384 + 256 bytes of ciphertext (incl. tag).
|
/// Real TLS 1.3 allows up to 16384 + 256 bytes of ciphertext (incl. tag).
|
||||||
const MAX_TLS_PAYLOAD: usize = 16384 + 256;
|
const MAX_TLS_PAYLOAD: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
|
|
||||||
/// Maximum pending write buffer for one record remainder.
|
/// Maximum pending write buffer for one record remainder.
|
||||||
/// Note: we never queue unlimited amount of data here; state holds at most one record.
|
/// Note: we never queue unlimited amount of data here; state holds at most one record.
|
||||||
|
|
@ -93,10 +93,10 @@ impl TlsRecordHeader {
|
||||||
/// - We accept TLS 1.0 header version for ClientHello-like records (0x03 0x01),
|
/// - We accept TLS 1.0 header version for ClientHello-like records (0x03 0x01),
|
||||||
/// and TLS 1.2/1.3 style version bytes for the rest (we use TLS_VERSION = 0x03 0x03).
|
/// and TLS 1.2/1.3 style version bytes for the rest (we use TLS_VERSION = 0x03 0x03).
|
||||||
/// - For Application Data, Telegram FakeTLS may send payload length up to
|
/// - For Application Data, Telegram FakeTLS may send payload length up to
|
||||||
/// MAX_TLS_CHUNK_SIZE (16384 + 256).
|
/// MAX_TLS_CIPHERTEXT_SIZE (16384 + 256).
|
||||||
/// - For other record types we keep stricter bounds to avoid memory abuse.
|
/// - For other record types we keep stricter bounds to avoid memory abuse.
|
||||||
fn validate(&self) -> Result<()> {
|
fn validate(&self) -> Result<()> {
|
||||||
// Version: accept TLS 1.0 header (ClientHello quirk) and TLS_VERSION (0x0303).
|
// Version precheck: only 0x0301 and 0x0303 are recognized at all.
|
||||||
if self.version != [0x03, 0x01] && self.version != TLS_VERSION {
|
if self.version != [0x03, 0x01] && self.version != TLS_VERSION {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
|
|
@ -104,31 +104,75 @@ impl TlsRecordHeader {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Narrow FakeTLS wire profile: TLS 1.0 compatibility header is allowed
|
||||||
|
// only on Handshake records (ClientHello compatibility quirk).
|
||||||
|
if self.record_type != TLS_RECORD_HANDSHAKE && self.version != TLS_VERSION {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"invalid TLS version for record type 0x{:02x}: {:02x?}",
|
||||||
|
self.record_type,
|
||||||
|
self.version
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let len = self.length as usize;
|
let len = self.length as usize;
|
||||||
|
|
||||||
// Length checks depend on record type.
|
// Length checks depend on record type.
|
||||||
// Telegram FakeTLS: ApplicationData length may be 16384 + 256.
|
// Telegram FakeTLS: ApplicationData may use ciphertext envelope limit,
|
||||||
|
// while control records stay structurally strict to reduce probe surface.
|
||||||
match self.record_type {
|
match self.record_type {
|
||||||
TLS_RECORD_APPLICATION => {
|
TLS_RECORD_APPLICATION => {
|
||||||
if len > MAX_TLS_CHUNK_SIZE {
|
if len == 0 || len > MAX_TLS_CIPHERTEXT_SIZE {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
format!("TLS record too large: {} bytes (max {})", len, MAX_TLS_CHUNK_SIZE),
|
format!(
|
||||||
|
"invalid TLS application data length: {} (min 1, max {})",
|
||||||
|
len,
|
||||||
|
MAX_TLS_CIPHERTEXT_SIZE
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeCipherSpec/Alert/Handshake should never be that large for our usage
|
TLS_RECORD_CHANGE_CIPHER => {
|
||||||
// (post-handshake we don't expect Handshake at all).
|
if len != 1 {
|
||||||
// Keep strict to reduce attack surface.
|
|
||||||
_ => {
|
|
||||||
if len > MAX_TLS_PAYLOAD {
|
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
format!("TLS control record too large: {} bytes (max {})", len, MAX_TLS_PAYLOAD),
|
format!("invalid TLS ChangeCipherSpec length: {} (expected 1)", len),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TLS_RECORD_ALERT => {
|
||||||
|
if len != 2 {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
format!("invalid TLS alert length: {} (expected 2)", len),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TLS_RECORD_HANDSHAKE => {
|
||||||
|
if len < 4 || len > MAX_TLS_PLAINTEXT_SIZE {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"invalid TLS handshake length: {} (min 4, max {})",
|
||||||
|
len,
|
||||||
|
MAX_TLS_PLAINTEXT_SIZE
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
format!("unknown TLS record type: 0x{:02x}", self.record_type),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -592,10 +636,10 @@ impl StreamState for TlsWriterState {
|
||||||
|
|
||||||
/// Writer that wraps bytes into TLS 1.3 Application Data records.
|
/// Writer that wraps bytes into TLS 1.3 Application Data records.
|
||||||
///
|
///
|
||||||
/// We chunk outgoing data into records of <= 16384 payload bytes (MAX_TLS_PAYLOAD).
|
/// We chunk outgoing data into records of <= MAX_TLS_CIPHERTEXT_SIZE payload bytes.
|
||||||
/// We do not try to mimic AEAD overhead on the wire; Telegram clients accept it.
|
/// We do not try to mimic AEAD overhead on the wire; Telegram clients accept it.
|
||||||
/// If you want to be more camouflage-accurate later, you could add optional padding
|
/// If you want to be more camouflage-accurate later, you could add optional padding
|
||||||
/// to produce records sized closer to MAX_TLS_CHUNK_SIZE.
|
/// to produce records sized closer to MAX_TLS_CIPHERTEXT_SIZE.
|
||||||
pub struct FakeTlsWriter<W> {
|
pub struct FakeTlsWriter<W> {
|
||||||
upstream: W,
|
upstream: W,
|
||||||
state: TlsWriterState,
|
state: TlsWriterState,
|
||||||
|
|
@ -831,7 +875,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for FakeTlsWriter<W> {
|
||||||
impl<W: AsyncWrite + Unpin> FakeTlsWriter<W> {
|
impl<W: AsyncWrite + Unpin> FakeTlsWriter<W> {
|
||||||
/// Write all data wrapped in TLS records.
|
/// Write all data wrapped in TLS records.
|
||||||
///
|
///
|
||||||
/// Convenience method that chunks into <= 16384 records.
|
/// Convenience method that chunks into <= MAX_TLS_CIPHERTEXT_SIZE records.
|
||||||
pub async fn write_all_tls(&mut self, data: &[u8]) -> Result<()> {
|
pub async fn write_all_tls(&mut self, data: &[u8]) -> Result<()> {
|
||||||
let mut written = 0;
|
let mut written = 0;
|
||||||
while written < data.len() {
|
while written < data.len() {
|
||||||
|
|
@ -846,6 +890,10 @@ impl<W: AsyncWrite + Unpin> FakeTlsWriter<W> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tls_stream_size_adversarial_tests.rs"]
|
||||||
|
mod size_adversarial_tests;
|
||||||
|
|
||||||
// ============= Tests =============
|
// ============= Tests =============
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,579 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::protocol::constants::MAX_TLS_PLAINTEXT_SIZE;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_record_above_plaintext_limit_must_be_rejected_early() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: (MAX_TLS_PLAINTEXT_SIZE + 1) as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"control-plane handshake record > MAX_TLS_PLAINTEXT_SIZE must fail closed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alert_record_above_plaintext_limit_must_be_rejected_early() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_ALERT,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: (MAX_TLS_PLAINTEXT_SIZE + 1) as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"TLS alert record > MAX_TLS_PLAINTEXT_SIZE must be rejected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ccs_record_len_not_equal_one_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_CHANGE_CIPHER,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"ChangeCipherSpec length must be exactly 1 byte in compat mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_record_len_zero_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"zero-length handshake record is structurally invalid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_record_len_one_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"tiny handshake record must be rejected to avoid malformed parser states"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_record_len_four_is_accepted() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_ok(),
|
||||||
|
"4-byte handshake payload is the minimum carrying handshake header"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_record_at_plaintext_limit_is_accepted() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: MAX_TLS_PLAINTEXT_SIZE as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_ok(),
|
||||||
|
"handshake record at plaintext RFC limit must be accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_record_at_ciphertext_limit_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: MAX_TLS_CIPHERTEXT_SIZE as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"control-plane handshake must never use ciphertext upper bound"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alert_record_len_zero_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_ALERT,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"TLS alert must always carry level+description bytes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alert_record_len_one_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_ALERT,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"one-byte TLS alert is malformed and must fail closed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alert_record_len_two_is_accepted() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_ALERT,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_ok(),
|
||||||
|
"standard TLS alert shape should be accepted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alert_record_len_three_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_ALERT,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"oversized plaintext alert should be rejected to avoid parser confusion"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ccs_record_len_zero_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_CHANGE_CIPHER,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"ChangeCipherSpec with zero length is malformed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ccs_record_len_one_is_accepted() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_CHANGE_CIPHER,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_ok(),
|
||||||
|
"ChangeCipherSpec compat record length must be accepted only for len=1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ccs_record_len_at_plaintext_limit_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_CHANGE_CIPHER,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: MAX_TLS_PLAINTEXT_SIZE as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"oversized CCS control frame must fail closed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_record_type_small_len_must_be_rejected_early() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: 0x19,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"unknown TLS record type should be rejected during header validation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_record_type_large_len_must_be_rejected_early() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: 0x7f,
|
||||||
|
version: TLS_VERSION,
|
||||||
|
length: MAX_TLS_CIPHERTEXT_SIZE as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"unknown record type with large payload must fail before body allocation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_tls10_header_with_plaintext_plus_one_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_HANDSHAKE,
|
||||||
|
version: [0x03, 0x01],
|
||||||
|
length: (MAX_TLS_PLAINTEXT_SIZE + 1) as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"TLS 1.0 compatibility header must not bypass plaintext size cap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alert_tls10_header_with_invalid_len_must_be_rejected() {
|
||||||
|
let header = TlsRecordHeader {
|
||||||
|
record_type: TLS_RECORD_ALERT,
|
||||||
|
version: [0x03, 0x01],
|
||||||
|
length: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
header.validate().is_err(),
|
||||||
|
"TLS 1.0 compatibility header must not bypass strict alert framing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validates(record_type: u8, version: [u8; 2], length: u16) -> bool {
|
||||||
|
TlsRecordHeader {
|
||||||
|
record_type,
|
||||||
|
version,
|
||||||
|
length,
|
||||||
|
}
|
||||||
|
.validate()
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! expect_reject {
|
||||||
|
($name:ident, $record_type:expr, $version:expr, $length:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
assert!(
|
||||||
|
!validates($record_type, $version, $length),
|
||||||
|
"expected reject for type=0x{:02x} version={:02x?} len={}",
|
||||||
|
$record_type,
|
||||||
|
$version,
|
||||||
|
$length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! expect_accept {
|
||||||
|
($name:ident, $record_type:expr, $version:expr, $length:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
assert!(
|
||||||
|
validates($record_type, $version, $length),
|
||||||
|
"expected accept for type=0x{:02x} version={:02x?} len={}",
|
||||||
|
$record_type,
|
||||||
|
$version,
|
||||||
|
$length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_reject!(appdata_zero_len_must_be_rejected, TLS_RECORD_APPLICATION, TLS_VERSION, 0);
|
||||||
|
expect_accept!(appdata_one_len_is_accepted, TLS_RECORD_APPLICATION, TLS_VERSION, 1);
|
||||||
|
expect_accept!(appdata_small_len_is_accepted, TLS_RECORD_APPLICATION, TLS_VERSION, 32);
|
||||||
|
expect_accept!(appdata_medium_len_is_accepted, TLS_RECORD_APPLICATION, TLS_VERSION, 1024);
|
||||||
|
expect_accept!(appdata_plaintext_limit_is_accepted, TLS_RECORD_APPLICATION, TLS_VERSION, MAX_TLS_PLAINTEXT_SIZE as u16);
|
||||||
|
expect_accept!(appdata_ciphertext_limit_is_accepted, TLS_RECORD_APPLICATION, TLS_VERSION, MAX_TLS_CIPHERTEXT_SIZE as u16);
|
||||||
|
expect_reject!(appdata_ciphertext_plus_one_must_be_rejected, TLS_RECORD_APPLICATION, TLS_VERSION, (MAX_TLS_CIPHERTEXT_SIZE as u16) + 1);
|
||||||
|
|
||||||
|
expect_reject!(appdata_tls10_header_len_one_must_be_rejected, TLS_RECORD_APPLICATION, [0x03, 0x01], 1);
|
||||||
|
expect_reject!(appdata_tls10_header_medium_must_be_rejected, TLS_RECORD_APPLICATION, [0x03, 0x01], 1024);
|
||||||
|
expect_reject!(appdata_tls10_header_ciphertext_limit_must_be_rejected, TLS_RECORD_APPLICATION, [0x03, 0x01], MAX_TLS_CIPHERTEXT_SIZE as u16);
|
||||||
|
|
||||||
|
expect_reject!(ccs_tls10_header_len_one_must_be_rejected, TLS_RECORD_CHANGE_CIPHER, [0x03, 0x01], 1);
|
||||||
|
expect_reject!(ccs_tls10_header_len_zero_must_be_rejected, TLS_RECORD_CHANGE_CIPHER, [0x03, 0x01], 0);
|
||||||
|
expect_reject!(ccs_tls10_header_len_two_must_be_rejected, TLS_RECORD_CHANGE_CIPHER, [0x03, 0x01], 2);
|
||||||
|
|
||||||
|
expect_reject!(alert_tls10_header_len_two_must_be_rejected, TLS_RECORD_ALERT, [0x03, 0x01], 2);
|
||||||
|
expect_reject!(alert_tls10_header_len_one_must_be_rejected, TLS_RECORD_ALERT, [0x03, 0x01], 1);
|
||||||
|
expect_reject!(alert_tls10_header_len_three_must_be_rejected, TLS_RECORD_ALERT, [0x03, 0x01], 3);
|
||||||
|
|
||||||
|
expect_accept!(handshake_tls10_header_min_len_is_accepted, TLS_RECORD_HANDSHAKE, [0x03, 0x01], 4);
|
||||||
|
expect_accept!(handshake_tls10_header_plaintext_limit_is_accepted, TLS_RECORD_HANDSHAKE, [0x03, 0x01], MAX_TLS_PLAINTEXT_SIZE as u16);
|
||||||
|
expect_reject!(handshake_tls10_header_too_small_must_be_rejected, TLS_RECORD_HANDSHAKE, [0x03, 0x01], 3);
|
||||||
|
expect_reject!(handshake_tls10_header_too_large_must_be_rejected, TLS_RECORD_HANDSHAKE, [0x03, 0x01], (MAX_TLS_PLAINTEXT_SIZE as u16) + 1);
|
||||||
|
|
||||||
|
expect_reject!(unknown_type_tls13_zero_must_be_rejected, 0x00, TLS_VERSION, 0);
|
||||||
|
expect_reject!(unknown_type_tls13_small_must_be_rejected, 0x13, TLS_VERSION, 32);
|
||||||
|
expect_reject!(unknown_type_tls13_large_must_be_rejected, 0xfe, TLS_VERSION, MAX_TLS_CIPHERTEXT_SIZE as u16);
|
||||||
|
expect_reject!(unknown_type_tls10_small_must_be_rejected, 0x13, [0x03, 0x01], 32);
|
||||||
|
|
||||||
|
expect_reject!(appdata_invalid_version_0302_must_be_rejected, TLS_RECORD_APPLICATION, [0x03, 0x02], 128);
|
||||||
|
expect_reject!(handshake_invalid_version_0302_must_be_rejected, TLS_RECORD_HANDSHAKE, [0x03, 0x02], 128);
|
||||||
|
expect_reject!(alert_invalid_version_0302_must_be_rejected, TLS_RECORD_ALERT, [0x03, 0x02], 2);
|
||||||
|
expect_reject!(ccs_invalid_version_0302_must_be_rejected, TLS_RECORD_CHANGE_CIPHER, [0x03, 0x02], 1);
|
||||||
|
|
||||||
|
expect_reject!(appdata_invalid_version_0304_must_be_rejected, TLS_RECORD_APPLICATION, [0x03, 0x04], 128);
|
||||||
|
expect_reject!(handshake_invalid_version_0304_must_be_rejected, TLS_RECORD_HANDSHAKE, [0x03, 0x04], 128);
|
||||||
|
expect_reject!(alert_invalid_version_0304_must_be_rejected, TLS_RECORD_ALERT, [0x03, 0x04], 2);
|
||||||
|
expect_reject!(ccs_invalid_version_0304_must_be_rejected, TLS_RECORD_CHANGE_CIPHER, [0x03, 0x04], 1);
|
||||||
|
|
||||||
|
expect_accept!(handshake_tls13_len_5_is_accepted, TLS_RECORD_HANDSHAKE, TLS_VERSION, 5);
|
||||||
|
expect_accept!(appdata_tls13_len_16385_is_accepted, TLS_RECORD_APPLICATION, TLS_VERSION, (MAX_TLS_PLAINTEXT_SIZE as u16) + 1);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matrix_version_policy_is_strict_and_deterministic() {
|
||||||
|
let versions = [[0x03, 0x01], TLS_VERSION, [0x03, 0x02], [0x03, 0x04], [0x00, 0x00]];
|
||||||
|
let record_types = [
|
||||||
|
TLS_RECORD_APPLICATION,
|
||||||
|
TLS_RECORD_CHANGE_CIPHER,
|
||||||
|
TLS_RECORD_ALERT,
|
||||||
|
TLS_RECORD_HANDSHAKE,
|
||||||
|
];
|
||||||
|
|
||||||
|
for version in versions {
|
||||||
|
for record_type in record_types {
|
||||||
|
let len = match record_type {
|
||||||
|
TLS_RECORD_APPLICATION => 1,
|
||||||
|
TLS_RECORD_CHANGE_CIPHER => 1,
|
||||||
|
TLS_RECORD_ALERT => 2,
|
||||||
|
TLS_RECORD_HANDSHAKE => 4,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let accepted = validates(record_type, version, len);
|
||||||
|
let expected = if version == TLS_VERSION {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
version == [0x03, 0x01] && record_type == TLS_RECORD_HANDSHAKE
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
accepted, expected,
|
||||||
|
"version policy mismatch for type=0x{:02x} version={:02x?}",
|
||||||
|
record_type, version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn appdata_partition_property_holds_for_all_u16_edges() {
|
||||||
|
for len in [0u16, 1, 2, 3, 64, 255, 1024, 4096, 8192, 16_384, 16_385, 16_640, 16_641, u16::MAX] {
|
||||||
|
let accepted = validates(TLS_RECORD_APPLICATION, TLS_VERSION, len);
|
||||||
|
let expected = len >= 1 && usize::from(len) <= MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
|
assert_eq!(accepted, expected, "unexpected appdata decision for len={len}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handshake_partition_property_holds_for_all_u16_edges() {
|
||||||
|
for len in [0u16, 1, 2, 3, 4, 5, 64, 255, 1024, 4096, 8192, 16_383, 16_384, 16_385, u16::MAX] {
|
||||||
|
let accepted_tls13 = validates(TLS_RECORD_HANDSHAKE, TLS_VERSION, len);
|
||||||
|
let accepted_tls10 = validates(TLS_RECORD_HANDSHAKE, [0x03, 0x01], len);
|
||||||
|
let expected = (4..=MAX_TLS_PLAINTEXT_SIZE).contains(&usize::from(len));
|
||||||
|
|
||||||
|
assert_eq!(accepted_tls13, expected, "TLS1.3 handshake mismatch for len={len}");
|
||||||
|
assert_eq!(accepted_tls10, expected, "TLS1.0 compat handshake mismatch for len={len}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_record_exact_lengths_are_enforced_under_fuzzed_lengths() {
|
||||||
|
let mut x: u32 = 0xC0FFEE11;
|
||||||
|
for _ in 0..5000 {
|
||||||
|
x = x.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||||
|
let len = (x & 0xFFFF) as u16;
|
||||||
|
|
||||||
|
let ccs_ok = validates(TLS_RECORD_CHANGE_CIPHER, TLS_VERSION, len);
|
||||||
|
let alert_ok = validates(TLS_RECORD_ALERT, TLS_VERSION, len);
|
||||||
|
|
||||||
|
assert_eq!(ccs_ok, len == 1, "ccs length gate mismatch for len={len}");
|
||||||
|
assert_eq!(alert_ok, len == 2, "alert length gate mismatch for len={len}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_record_types_never_validate_under_supported_versions() {
|
||||||
|
for record_type in 0u8..=255 {
|
||||||
|
if matches!(record_type, TLS_RECORD_APPLICATION | TLS_RECORD_CHANGE_CIPHER | TLS_RECORD_ALERT | TLS_RECORD_HANDSHAKE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!validates(record_type, TLS_VERSION, 1),
|
||||||
|
"unknown type must not validate under TLS_VERSION: 0x{record_type:02x}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!validates(record_type, [0x03, 0x01], 4),
|
||||||
|
"unknown type must not validate under TLS1.0 compat: 0x{record_type:02x}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reader_rejects_tls10_appdata_header_before_payload_processing() {
|
||||||
|
let (mut tx, rx) = tokio::io::duplex(128);
|
||||||
|
tx.write_all(&[TLS_RECORD_APPLICATION, 0x03, 0x01, 0x00, 0x01, 0xAB])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
let mut reader = FakeTlsReader::new(rx);
|
||||||
|
let mut out = [0u8; 1];
|
||||||
|
let err = reader.read(&mut out).await.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reader_rejects_zero_len_appdata_record() {
|
||||||
|
let (mut tx, rx) = tokio::io::duplex(128);
|
||||||
|
tx.write_all(&[TLS_RECORD_APPLICATION, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x00])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
let mut reader = FakeTlsReader::new(rx);
|
||||||
|
let mut out = [0u8; 1];
|
||||||
|
let err = reader.read(&mut out).await.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reader_accepts_single_byte_tls13_appdata_and_yields_payload() {
|
||||||
|
let (mut tx, rx) = tokio::io::duplex(128);
|
||||||
|
tx.write_all(&[TLS_RECORD_APPLICATION, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x01, 0x5A])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
let mut reader = FakeTlsReader::new(rx);
|
||||||
|
let mut out = [0u8; 1];
|
||||||
|
let n = reader.read(&mut out).await.unwrap();
|
||||||
|
assert_eq!(n, 1);
|
||||||
|
assert_eq!(out[0], 0x5A);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reader_rejects_tls10_alert_even_with_structural_length() {
|
||||||
|
let (mut tx, rx) = tokio::io::duplex(128);
|
||||||
|
tx.write_all(&[TLS_RECORD_ALERT, 0x03, 0x01, 0x00, 0x02, 0x02, 0x28])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
let mut reader = FakeTlsReader::new(rx);
|
||||||
|
let mut out = [0u8; 8];
|
||||||
|
let err = reader.read(&mut out).await.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reader_rejects_unknown_record_type_fast() {
|
||||||
|
let (mut tx, rx) = tokio::io::duplex(128);
|
||||||
|
tx.write_all(&[0x7f, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x01, 0x01])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
let mut reader = FakeTlsReader::new(rx);
|
||||||
|
let mut out = [0u8; 8];
|
||||||
|
let err = reader.read(&mut out).await.unwrap_err();
|
||||||
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reader_preserves_data_after_valid_ccs_then_valid_appdata() {
|
||||||
|
let (mut tx, rx) = tokio::io::duplex(256);
|
||||||
|
tx.write_all(&[
|
||||||
|
TLS_RECORD_CHANGE_CIPHER,
|
||||||
|
TLS_VERSION[0],
|
||||||
|
TLS_VERSION[1],
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x01,
|
||||||
|
TLS_RECORD_APPLICATION,
|
||||||
|
TLS_VERSION[0],
|
||||||
|
TLS_VERSION[1],
|
||||||
|
0x00,
|
||||||
|
0x03,
|
||||||
|
0xDE,
|
||||||
|
0xAD,
|
||||||
|
0xBE,
|
||||||
|
])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tx.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
let mut reader = FakeTlsReader::new(rx);
|
||||||
|
let mut out = [0u8; 3];
|
||||||
|
let n = reader.read(&mut out).await.unwrap();
|
||||||
|
assert_eq!(n, 3);
|
||||||
|
assert_eq!(out, [0xDE, 0xAD, 0xBE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deterministic_lcg_never_breaks_validation_invariants() {
|
||||||
|
let mut x: u64 = 0xD1A5_CE55_0BAD_F00D;
|
||||||
|
for _ in 0..20000 {
|
||||||
|
x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||||
|
let record_type = (x & 0xFF) as u8;
|
||||||
|
let version = match (x >> 8) & 0x3 {
|
||||||
|
0 => TLS_VERSION,
|
||||||
|
1 => [0x03, 0x01],
|
||||||
|
2 => [0x03, 0x02],
|
||||||
|
_ => [0x03, 0x04],
|
||||||
|
};
|
||||||
|
let len = ((x >> 16) & 0xFFFF) as u16;
|
||||||
|
|
||||||
|
let accepted = validates(record_type, version, len);
|
||||||
|
|
||||||
|
let expected = match record_type {
|
||||||
|
TLS_RECORD_APPLICATION => {
|
||||||
|
version == TLS_VERSION && len >= 1 && usize::from(len) <= MAX_TLS_CIPHERTEXT_SIZE
|
||||||
|
}
|
||||||
|
TLS_RECORD_CHANGE_CIPHER => version == TLS_VERSION && len == 1,
|
||||||
|
TLS_RECORD_ALERT => version == TLS_VERSION && len == 2,
|
||||||
|
TLS_RECORD_HANDSHAKE => {
|
||||||
|
(version == TLS_VERSION || version == [0x03, 0x01])
|
||||||
|
&& (4..=MAX_TLS_PLAINTEXT_SIZE).contains(&usize::from(len))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
accepted, expected,
|
||||||
|
"invariant mismatch: type=0x{record_type:02x} version={version:02x?} len={len}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use crate::crypto::{sha256_hmac, SecureRandom};
|
use crate::crypto::{sha256_hmac, SecureRandom};
|
||||||
use crate::protocol::constants::{
|
use crate::protocol::constants::{
|
||||||
|
MAX_TLS_CIPHERTEXT_SIZE,
|
||||||
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE, TLS_VERSION,
|
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE, TLS_VERSION,
|
||||||
};
|
};
|
||||||
use crate::protocol::tls::{TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key};
|
use crate::protocol::tls::{TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key};
|
||||||
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo, TlsProfileSource};
|
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo, TlsProfileSource};
|
||||||
|
|
||||||
const MIN_APP_DATA: usize = 64;
|
const MIN_APP_DATA: usize = 64;
|
||||||
const MAX_APP_DATA: usize = 16640; // RFC 8446 §5.2 allows up to 2^14 + 256
|
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||||
|
|
||||||
fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
|
fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
|
||||||
sizes
|
sizes
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue