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:
David Osipov
2026-03-20 21:00:36 +04:00
parent 801f670827
commit 3abde52de8
11 changed files with 713 additions and 54 deletions

View File

@@ -12,7 +12,7 @@
//! Telegram MTProto proxy "FakeTLS" mode uses a TLS-looking outer layer for
//! domain fronting / traffic camouflage. iOS Telegram clients are known to
//! 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:
//! - Explicit state machines for all async operations
@@ -23,14 +23,13 @@
//! - Proper handling of all TLS record types
//!
//! Important nuance (Telegram FakeTLS):
//! - The TLS spec limits "plaintext fragments" to 2^14 (16384) bytes.
//! - However, the on-the-wire record length can exceed 16384 because TLS 1.3
//! - The TLS spec limits "plaintext fragments" to MAX_TLS_PLAINTEXT_SIZE bytes.
//! - 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.
//! - 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
//! MAX_TLS_CHUNK_SIZE.
//! with length up to MAX_TLS_CIPHERTEXT_SIZE bytes (RFC 8446 §5.2).
//!
//! 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"
//! and uploads from iOS will break (media/file sending), while small traffic
//! may still work.
@@ -42,10 +41,11 @@ use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt, ReadBuf};
use crate::protocol::constants::{
MAX_TLS_PLAINTEXT_SIZE,
TLS_VERSION,
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER,
TLS_RECORD_HANDSHAKE, TLS_RECORD_ALERT,
MAX_TLS_CHUNK_SIZE,
MAX_TLS_CIPHERTEXT_SIZE,
};
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.
/// 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.
/// 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),
/// 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
/// MAX_TLS_CHUNK_SIZE (16384 + 256).
/// MAX_TLS_CIPHERTEXT_SIZE (16384 + 256).
/// - For other record types we keep stricter bounds to avoid memory abuse.
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 {
return Err(Error::new(
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;
// 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 {
TLS_RECORD_APPLICATION => {
if len > MAX_TLS_CHUNK_SIZE {
if len == 0 || len > MAX_TLS_CIPHERTEXT_SIZE {
return Err(Error::new(
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
// (post-handshake we don't expect Handshake at all).
// Keep strict to reduce attack surface.
_ => {
if len > MAX_TLS_PAYLOAD {
TLS_RECORD_CHANGE_CIPHER => {
if len != 1 {
return Err(Error::new(
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(())
@@ -592,10 +636,10 @@ impl StreamState for TlsWriterState {
/// 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.
/// 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> {
upstream: W,
state: TlsWriterState,
@@ -831,7 +875,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for FakeTlsWriter<W> {
impl<W: AsyncWrite + Unpin> FakeTlsWriter<W> {
/// 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<()> {
let mut written = 0;
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 =============
#[cfg(test)]

View File

@@ -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}"
);
}
}