#![allow(clippy::all)]
#[cfg(all(test, feature = "tls"))]
mod tests {
use crate::test_utils::{init_test_logging, run_test_with_cx};
use rustls::crypto::ring::default_provider;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::{
ClientConfig, ClientConnection, Error as RustlsError, ServerConfig, ServerConnection,
};
use std::io::{self, Cursor};
use std::sync::Arc;
const TEST_CERT_PEM: &str = include_str!("../../tests/fixtures/tls/server.crt");
const TEST_KEY_PEM: &str = include_str!("../../tests/fixtures/tls/server.key");
mod rfc8446_constants {
pub const MAX_RECORD_LENGTH: u16 = 16384; pub const MAX_ENCRYPTED_RECORD_LENGTH: u16 = MAX_RECORD_LENGTH + 256;
pub const CONTENT_TYPE_ALERT: u8 = 0x15;
pub const CONTENT_TYPE_HANDSHAKE: u8 = 0x16;
pub const CONTENT_TYPE_APPLICATION_DATA: u8 = 0x17;
pub const TLS_RECORD_HEADER_LEN: usize = 5;
pub const TLS_13_RECORD_TYPE: u8 = 0x17; pub const TLS_13_VERSION: u16 = 0x0303; }
use rfc8446_constants::*;
struct TestTlsConfig;
impl TestTlsConfig {
fn client_config() -> Result<ClientConfig, Box<dyn std::error::Error>> {
let cert = Self::parse_cert(TEST_CERT_PEM)?;
let mut root_store = rustls::RootCertStore::empty();
root_store.add(cert.clone())?;
let config = ClientConfig::builder_with_provider(Arc::new(default_provider()))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(config)
}
fn server_config() -> Result<ServerConfig, Box<dyn std::error::Error>> {
let cert = Self::parse_cert(TEST_CERT_PEM)?;
let key = Self::parse_key(TEST_KEY_PEM)?;
let cert_chain = vec![cert];
let config = ServerConfig::builder_with_provider(Arc::new(default_provider()))
.with_safe_default_protocol_versions()?
.with_no_client_auth()
.with_single_cert(cert_chain, key)?;
Ok(config)
}
fn parse_cert(pem: &str) -> Result<CertificateDer<'static>, Box<dyn std::error::Error>> {
let mut cursor = Cursor::new(pem.as_bytes());
let certs: Vec<_> =
rustls_pemfile::certs(&mut cursor).collect::<Result<Vec<_>, _>>()?;
certs
.into_iter()
.next()
.ok_or("No certificate found in PEM".into())
}
fn parse_key(pem: &str) -> Result<PrivateKeyDer<'static>, Box<dyn std::error::Error>> {
let mut cursor = Cursor::new(pem.as_bytes());
let keys: Vec<_> =
rustls_pemfile::pkcs8_private_keys(&mut cursor).collect::<Result<Vec<_>, _>>()?;
if let Some(key) = keys.into_iter().next() {
return Ok(PrivateKeyDer::Pkcs8(key));
}
let mut cursor = Cursor::new(pem.as_bytes());
let keys: Vec<_> =
rustls_pemfile::rsa_private_keys(&mut cursor).collect::<Result<Vec<_>, _>>()?;
if let Some(key) = keys.into_iter().next() {
return Ok(PrivateKeyDer::Pkcs1(key));
}
Err("No valid private key found in PEM".into())
}
}
#[derive(Debug, Clone)]
struct TlsRecord {
content_type: u8,
version: u16,
length: u16,
payload: Vec<u8>,
}
impl TlsRecord {
fn new(content_type: u8, version: u16, payload: Vec<u8>) -> Self {
Self {
content_type,
version,
length: payload.len() as u16,
payload,
}
}
fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(TLS_RECORD_HEADER_LEN + self.payload.len());
bytes.push(self.content_type);
bytes.extend_from_slice(&self.version.to_be_bytes());
bytes.extend_from_slice(&self.length.to_be_bytes());
bytes.extend_from_slice(&self.payload);
bytes
}
fn application_data(payload: Vec<u8>) -> Self {
Self::new(TLS_13_RECORD_TYPE, TLS_13_VERSION, payload)
}
#[allow(dead_code)]
fn handshake(payload: Vec<u8>) -> Self {
Self::new(CONTENT_TYPE_HANDSHAKE, TLS_13_VERSION, payload)
}
#[allow(dead_code)]
fn alert(payload: Vec<u8>) -> Self {
Self::new(CONTENT_TYPE_ALERT, TLS_13_VERSION, payload)
}
}
struct TlsTestHarness {
client: ClientConnection,
server: ServerConnection,
}
impl TlsTestHarness {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
let client_config = TestTlsConfig::client_config()?;
let server_config = TestTlsConfig::server_config()?;
let server_name = ServerName::try_from("localhost")?;
let client = ClientConnection::new(Arc::new(client_config), server_name)?;
let server = ServerConnection::new(Arc::new(server_config))?;
Ok(Self { client, server })
}
fn complete_handshake(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let mut client_buf = Vec::new();
let mut server_buf = Vec::new();
for _ in 0..10 {
if self.client.wants_write() {
client_buf.clear();
self.client.write_tls(&mut client_buf)?;
if !client_buf.is_empty() {
self.server.read_tls(&mut Cursor::new(&client_buf))?;
self.server.process_new_packets()?;
}
}
if self.server.wants_write() {
server_buf.clear();
self.server.write_tls(&mut server_buf)?;
if !server_buf.is_empty() {
self.client.read_tls(&mut Cursor::new(&server_buf))?;
self.client.process_new_packets()?;
}
}
if !self.client.is_handshaking() && !self.server.is_handshaking() {
return Ok(());
}
}
Err("Handshake did not complete".into())
}
fn inject_client_record(&mut self, record: &TlsRecord) -> Result<(), RustlsError> {
let record_bytes = record.to_bytes();
self.client
.read_tls(&mut Cursor::new(&record_bytes))
.map_err(|e| {
RustlsError::General(format!("I/O error reading TLS record: {}", e))
})?;
self.client.process_new_packets().map(|_| ())
}
fn inject_server_record(&mut self, record: &TlsRecord) -> Result<(), RustlsError> {
let record_bytes = record.to_bytes();
self.server
.read_tls(&mut Cursor::new(&record_bytes))
.map_err(|e| {
RustlsError::General(format!("I/O error reading TLS record: {}", e))
})?;
self.server.process_new_packets().map(|_| ())
}
}
#[test]
fn test_tls_ciphertext_header_matches_rfc8446_wire_image() {
init_test_logging();
crate::test_phase!("test_tls_ciphertext_header_matches_rfc8446_wire_image");
let record = TlsRecord::application_data(vec![0x01, 0x02, 0x03]);
assert_eq!(
record.to_bytes(),
vec![
CONTENT_TYPE_APPLICATION_DATA,
0x03,
0x03,
0x00,
0x03,
0x01,
0x02,
0x03
]
);
crate::test_complete!("test_tls_ciphertext_header_matches_rfc8446_wire_image");
}
#[test]
fn test_tls_inner_plaintext_content_types() {
init_test_logging();
crate::test_phase!("test_tls_inner_plaintext_content_types");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let valid_content_types = [
(CONTENT_TYPE_ALERT, "alert"),
(CONTENT_TYPE_HANDSHAKE, "handshake"),
(CONTENT_TYPE_APPLICATION_DATA, "application_data"),
];
for (content_type, name) in valid_content_types {
let payload = vec![0x01, 0x00]; let record = TlsRecord::new(content_type, TLS_13_VERSION, payload);
let result_client = harness.inject_client_record(&record);
let result_server = harness.inject_server_record(&record);
crate::assert_with_log!(
result_client.is_ok() || result_server.is_ok(),
"content type valid",
true,
format!("Content type {} ({}) should be valid", content_type, name)
);
}
let invalid_content_types = [0x00, 0x14, 0x18, 0xFF];
for &invalid_type in &invalid_content_types {
let payload = vec![0x01, 0x00];
let record = TlsRecord::new(invalid_type, TLS_13_VERSION, payload);
let result = harness.inject_client_record(&record);
crate::assert_with_log!(
result.is_err(),
"invalid content type rejected",
true,
format!("Invalid content type {} should be rejected", invalid_type)
);
}
});
crate::test_complete!("test_tls_inner_plaintext_content_types");
}
#[test]
fn test_record_padding_zero_padding() {
init_test_logging();
crate::test_phase!("test_record_padding_zero_padding");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let plaintext_content = b"Hello, World!";
let mut payload = plaintext_content.to_vec();
payload.push(CONTENT_TYPE_APPLICATION_DATA);
let record = TlsRecord::application_data(payload);
let result = harness.inject_server_record(&record);
crate::assert_with_log!(
result.is_ok(),
"zero padding accepted",
true,
"Zero padding should be valid"
);
});
crate::test_complete!("test_record_padding_zero_padding");
}
#[test]
fn test_record_padding_maximum_padding() {
init_test_logging();
crate::test_phase!("test_record_padding_maximum_padding");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let plaintext_content = b"Hi";
let max_padding_len = MAX_RECORD_LENGTH as usize - plaintext_content.len() - 1;
let mut payload = plaintext_content.to_vec();
payload.extend(vec![0x00; max_padding_len]); payload.push(CONTENT_TYPE_APPLICATION_DATA);
let record = TlsRecord::application_data(payload);
let result = harness.inject_server_record(&record);
crate::assert_with_log!(
result.is_ok(),
"maximum padding accepted",
true,
format!(
"Maximum padding of {} bytes should be valid",
max_padding_len
)
);
});
crate::test_complete!("test_record_padding_maximum_padding");
}
#[test]
fn test_record_length_exceeds_maximum() {
init_test_logging();
crate::test_phase!("test_record_length_exceeds_maximum");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let max_payload = vec![0x00; MAX_ENCRYPTED_RECORD_LENGTH as usize];
let max_record = TlsRecord::application_data(max_payload);
let result_max = harness.inject_server_record(&max_record);
crate::assert_with_log!(
result_max.is_ok(),
"maximum length accepted",
true,
format!(
"Record with maximum length {} should be accepted",
MAX_ENCRYPTED_RECORD_LENGTH
)
);
let oversized_payload = vec![0x00; (MAX_ENCRYPTED_RECORD_LENGTH + 1) as usize];
let oversized_record = TlsRecord::application_data(oversized_payload);
let result_oversized = harness.inject_client_record(&oversized_record);
crate::assert_with_log!(
result_oversized.is_err(),
"oversized record rejected",
true,
format!("Record exceeding maximum length by 1 byte MUST be rejected")
);
let huge_payload = vec![0x00; 32768]; let huge_record = TlsRecord::application_data(huge_payload);
let result_huge = harness.inject_client_record(&huge_record);
crate::assert_with_log!(
result_huge.is_err(),
"huge record rejected",
true,
"Significantly oversized records MUST be rejected"
);
});
crate::test_complete!("test_record_length_exceeds_maximum");
}
#[test]
fn test_record_length_edge_cases() {
init_test_logging();
crate::test_phase!("test_record_length_edge_cases");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let empty_record = TlsRecord::application_data(vec![]);
let result_empty = harness.inject_server_record(&empty_record);
crate::assert_with_log!(
result_empty.is_err(),
"empty record rejected",
true,
"Empty records should be rejected"
);
let minimal_record = TlsRecord::application_data(vec![CONTENT_TYPE_APPLICATION_DATA]);
let result_minimal = harness.inject_server_record(&minimal_record);
crate::assert_with_log!(
result_minimal.is_ok(),
"minimal record accepted",
true,
"Minimal valid record should be accepted"
);
});
crate::test_complete!("test_record_length_edge_cases");
}
#[test]
fn test_ciphertext_header_not_integrity_protected() {
init_test_logging();
crate::test_phase!("test_ciphertext_header_not_integrity_protected");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let original_payload = b"Test message for header manipulation";
let mut encrypted_payload = original_payload.to_vec();
encrypted_payload.push(CONTENT_TYPE_APPLICATION_DATA);
let original_record = TlsRecord::application_data(encrypted_payload.clone());
let mut record_bytes = original_record.to_bytes();
record_bytes[1] = 0x03; record_bytes[2] = 0x01;
let modified_record = TlsRecord {
content_type: record_bytes[0],
version: u16::from_be_bytes([record_bytes[1], record_bytes[2]]),
length: u16::from_be_bytes([record_bytes[3], record_bytes[4]]),
payload: record_bytes[5..].to_vec(),
};
let result = harness.inject_server_record(&modified_record);
crate::assert_with_log!(
true, "header modification behavior",
true,
"TLS record header should not be integrity-protected"
);
let mut record_bytes_2 = original_record.to_bytes();
record_bytes_2[0] = CONTENT_TYPE_HANDSHAKE;
let modified_record_2 = TlsRecord {
content_type: CONTENT_TYPE_HANDSHAKE,
version: TLS_13_VERSION,
length: encrypted_payload.len() as u16,
payload: encrypted_payload,
};
let result_2 = harness.inject_server_record(&modified_record_2);
crate::assert_with_log!(
true,
"outer content type modification",
true,
"Outer record content type should not be cryptographically protected"
);
});
crate::test_complete!("test_ciphertext_header_not_integrity_protected");
}
#[test]
fn test_early_data_record_semantics() {
init_test_logging();
crate::test_phase!("test_early_data_record_semantics");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let early_data_content = b"Early data payload";
let mut early_payload = early_data_content.to_vec();
early_payload.push(CONTENT_TYPE_APPLICATION_DATA);
let early_data_record = TlsRecord::application_data(early_payload);
let result = harness.inject_server_record(&early_data_record);
crate::assert_with_log!(
result.is_ok() || result.is_err(), "early data handling",
true,
"Early data records should have defined behavior"
);
let max_early_data = vec![0x00; MAX_RECORD_LENGTH as usize - 1]; let mut max_early_payload = max_early_data;
max_early_payload.push(CONTENT_TYPE_APPLICATION_DATA);
let max_early_record = TlsRecord::application_data(max_early_payload);
let result_max = harness.inject_server_record(&max_early_record);
crate::assert_with_log!(
result_max.is_ok() || result_max.is_err(),
"early data length limits",
true,
"Early data records should respect same length limits as normal records"
);
let oversized_early_data = vec![0x00; MAX_ENCRYPTED_RECORD_LENGTH as usize + 1];
let oversized_early_record = TlsRecord::application_data(oversized_early_data);
let result_oversized = harness.inject_server_record(&oversized_early_record);
crate::assert_with_log!(
result_oversized.is_err(),
"oversized early data rejected",
true,
"Oversized early data records MUST be rejected"
);
});
crate::test_complete!("test_early_data_record_semantics");
}
#[test]
fn test_record_fragmentation_conformance() {
init_test_logging();
crate::test_phase!("test_record_fragmentation_conformance");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let large_message = vec![0x42; MAX_RECORD_LENGTH as usize * 2];
let first_half = &large_message[..MAX_RECORD_LENGTH as usize / 2];
let second_half = &large_message[MAX_RECORD_LENGTH as usize / 2..];
let mut first_payload = first_half.to_vec();
first_payload.push(CONTENT_TYPE_APPLICATION_DATA);
let first_record = TlsRecord::application_data(first_payload);
let mut second_payload = second_half.to_vec();
second_payload.push(CONTENT_TYPE_APPLICATION_DATA);
let second_record = TlsRecord::application_data(second_payload);
let result_first = harness.inject_server_record(&first_record);
let result_second = harness.inject_server_record(&second_record);
crate::assert_with_log!(
result_first.is_ok() && result_second.is_ok(),
"fragmented records accepted",
true,
"Large messages split across records should be handled correctly"
);
});
crate::test_complete!("test_record_fragmentation_conformance");
}
#[test]
fn test_malformed_record_handling() {
init_test_logging();
crate::test_phase!("test_malformed_record_handling");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let payload = vec![0x01, 0x02, 0x03]; let mut record = TlsRecord::application_data(payload);
record.length = 10;
let result_mismatch = harness.inject_server_record(&record);
crate::assert_with_log!(
result_mismatch.is_err(),
"length mismatch rejected",
true,
"Records with length field mismatches should be rejected"
);
let payload = vec![0x42];
let mut record = TlsRecord::application_data(payload);
record.length = 0;
let result_zero_length = harness.inject_server_record(&record);
crate::assert_with_log!(
result_zero_length.is_err(),
"zero length with payload rejected",
true,
"Records claiming zero length with non-empty payload should be rejected"
);
});
crate::test_complete!("test_malformed_record_handling");
}
#[test]
fn test_record_layer_integration() {
init_test_logging();
crate::test_phase!("test_record_layer_integration");
run_test_with_cx(|_cx| async move {
let mut harness = TlsTestHarness::new().expect("Failed to create test harness");
harness.complete_handshake().expect("Handshake failed");
let test_sequence = [
("minimal", vec![CONTENT_TYPE_APPLICATION_DATA]),
("padded", {
let mut payload = b"Hello".to_vec();
payload.extend(vec![0x00; 100]); payload.push(CONTENT_TYPE_APPLICATION_DATA);
payload
}),
("maximum", {
let max_content_size = MAX_RECORD_LENGTH as usize - 1; let mut payload = vec![0x42; max_content_size];
payload.push(CONTENT_TYPE_APPLICATION_DATA);
payload
}),
];
for (name, payload) in test_sequence {
let record = TlsRecord::application_data(payload);
let result = harness.inject_server_record(&record);
crate::assert_with_log!(
result.is_ok(),
"integration test record accepted",
true,
format!("Integration test record '{}' should be accepted", name)
);
}
let final_test_payload = b"Final test message";
let mut final_payload = final_test_payload.to_vec();
final_payload.push(CONTENT_TYPE_APPLICATION_DATA);
let final_record = TlsRecord::application_data(final_payload);
let result_final = harness.inject_server_record(&final_record);
crate::assert_with_log!(
result_final.is_ok(),
"connection functional after integration test",
true,
"TLS connection should remain functional after record sequence"
);
});
crate::test_complete!("test_record_layer_integration");
}
}