use crate::Result;
use crate::constants::{
ALERT_LEVEL_FATAL, ALERT_UNRECOGNIZED_NAME, BUFFER_SIZE_MAX_TLS_RECORD, CONTENT_TYPE_ALERT,
CONTENT_TYPE_HANDSHAKE, DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT,
EXTENSION_EC_POINT_FORMATS, EXTENSION_SERVER_NAME, EXTENSION_SIGNATURE_ALGORITHMS,
EXTENSION_SUPPORTED_GROUPS, HANDSHAKE_TYPE_CLIENT_HELLO, VERSION_TLS_1_0, VERSION_TLS_1_2,
};
use crate::utils::network::Target;
use bytes::{BufMut, BytesMut};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IntoleranceTestResult {
pub extension_intolerance: bool,
pub version_intolerance: bool,
pub long_handshake_intolerance: bool,
pub incorrect_sni_alerts: bool,
pub uses_common_dh_primes: bool,
pub details: HashMap<String, String>,
}
pub struct IntoleranceTester {
target: Target,
connect_timeout: Duration,
read_timeout: Duration,
}
impl IntoleranceTester {
pub fn new(target: Target) -> Self {
Self {
target,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
read_timeout: DEFAULT_READ_TIMEOUT,
}
}
pub async fn test_all(&self) -> Result<IntoleranceTestResult> {
let mut result = IntoleranceTestResult::default();
result.extension_intolerance = self.test_extension_intolerance().await?;
if result.extension_intolerance {
result.details.insert(
"extension_intolerance".to_string(),
"Server rejects ClientHellos with certain extensions (bad)".to_string(),
);
}
result.version_intolerance = self.test_version_intolerance().await?;
if result.version_intolerance {
result.details.insert(
"version_intolerance".to_string(),
"Server rejects ClientHello with high version in record layer (bad)".to_string(),
);
}
result.long_handshake_intolerance = self.test_long_handshake_intolerance().await?;
if result.long_handshake_intolerance {
result.details.insert(
"long_handshake_intolerance".to_string(),
"Server rejects ClientHello > 256 bytes (bad)".to_string(),
);
}
result.incorrect_sni_alerts = self.test_sni_alerts().await?;
if result.incorrect_sni_alerts {
result.details.insert(
"incorrect_sni_alerts".to_string(),
"Server sends incorrect alert when SNI fails (bad)".to_string(),
);
}
result.uses_common_dh_primes = self.test_common_dh_primes().await?;
if result.uses_common_dh_primes {
result.details.insert(
"uses_common_dh_primes".to_string(),
"Server uses known weak DH primes (critical security issue)".to_string(),
);
}
Ok(result)
}
async fn test_extension_intolerance(&self) -> Result<bool> {
let minimal_hello = self.build_minimal_client_hello()?;
let minimal_response = self.send_client_hello(&minimal_hello).await;
let extended_hello = self.build_extended_client_hello()?;
let extended_response = self.send_client_hello(&extended_hello).await;
match (minimal_response, extended_response) {
(Ok(_), Err(_)) => {
Ok(true)
}
_ => {
Ok(false)
}
}
}
async fn test_version_intolerance(&self) -> Result<bool> {
let normal_hello = self.build_versioned_client_hello(0x0301)?; let normal_response = self.send_client_hello(&normal_hello).await;
let high_version_hello = self.build_versioned_client_hello(0x0303)?; let high_version_response = self.send_client_hello(&high_version_hello).await;
match (normal_response, high_version_response) {
(Ok(_), Err(_)) => {
Ok(true)
}
_ => {
Ok(false)
}
}
}
async fn test_long_handshake_intolerance(&self) -> Result<bool> {
let normal_hello = self.build_minimal_client_hello()?;
let normal_response = self.send_client_hello(&normal_hello).await;
let long_hello = self.build_long_client_hello()?;
let long_response = self.send_client_hello(&long_hello).await;
match (normal_response, long_response) {
(Ok(_), Err(_)) => {
Ok(true)
}
_ => {
Ok(false)
}
}
}
async fn test_sni_alerts(&self) -> Result<bool> {
let invalid_sni_hello = self.build_invalid_sni_client_hello()?;
match self.send_and_read_alert(&invalid_sni_hello).await {
Ok(Some(alert_code)) => {
Ok(alert_code != ALERT_UNRECOGNIZED_NAME)
}
_ => {
Ok(false)
}
}
}
async fn test_common_dh_primes(&self) -> Result<bool> {
let common_primes = Self::load_common_primes()?;
match self.extract_dh_prime().await {
Ok(Some(prime_hex)) => {
Ok(common_primes.contains(&prime_hex.to_uppercase()))
}
_ => {
Ok(false)
}
}
}
fn build_minimal_client_hello(&self) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
buf.put_u8(CONTENT_TYPE_HANDSHAKE);
buf.put_u16(VERSION_TLS_1_0);
let length_pos = buf.len();
buf.put_u16(0);
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
let hs_length_pos = buf.len();
buf.put_u8(0);
buf.put_u16(0);
buf.put_u16(VERSION_TLS_1_2);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs() as u32;
buf.put_u32(timestamp);
buf.put_slice(&[0u8; 28]);
buf.put_u8(0);
let ciphers = vec![
0xc02f, 0xc030, 0x009c, 0x009d, ];
buf.put_u16((ciphers.len() * 2) as u16);
for cipher in ciphers {
buf.put_u16(cipher);
}
buf.put_u8(1); buf.put_u8(0);
let total_length = buf.len() - length_pos - 2;
let hs_length = buf.len() - hs_length_pos - 3;
let mut result = buf.to_vec();
result[length_pos] = ((total_length >> 8) & 0xff) as u8;
result[length_pos + 1] = (total_length & 0xff) as u8;
result[hs_length_pos] = ((hs_length >> 16) & 0xff) as u8;
result[hs_length_pos + 1] = ((hs_length >> 8) & 0xff) as u8;
result[hs_length_pos + 2] = (hs_length & 0xff) as u8;
Ok(result)
}
fn build_extended_client_hello(&self) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
buf.put_u8(CONTENT_TYPE_HANDSHAKE);
buf.put_u16(VERSION_TLS_1_0);
let length_pos = buf.len();
buf.put_u16(0);
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
let hs_length_pos = buf.len();
buf.put_u8(0);
buf.put_u16(0);
buf.put_u16(VERSION_TLS_1_2);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs() as u32;
buf.put_u32(timestamp);
buf.put_slice(&[0u8; 28]);
buf.put_u8(0);
let ciphers = vec![0xc02f, 0xc030, 0x009c, 0x009d];
buf.put_u16((ciphers.len() * 2) as u16);
for cipher in ciphers {
buf.put_u16(cipher);
}
buf.put_u8(1);
buf.put_u8(0);
let mut extensions = BytesMut::new();
self.add_sni_extension(&mut extensions, &self.target.hostname);
self.add_supported_groups_extension(&mut extensions);
self.add_ec_point_formats_extension(&mut extensions);
self.add_signature_algorithms_extension(&mut extensions);
buf.put_u16(extensions.len() as u16);
buf.put_slice(&extensions);
let total_length = buf.len() - length_pos - 2;
let hs_length = buf.len() - hs_length_pos - 3;
let mut result = buf.to_vec();
result[length_pos] = ((total_length >> 8) & 0xff) as u8;
result[length_pos + 1] = (total_length & 0xff) as u8;
result[hs_length_pos] = ((hs_length >> 16) & 0xff) as u8;
result[hs_length_pos + 1] = ((hs_length >> 8) & 0xff) as u8;
result[hs_length_pos + 2] = (hs_length & 0xff) as u8;
Ok(result)
}
fn build_versioned_client_hello(&self, record_version: u16) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
buf.put_u8(CONTENT_TYPE_HANDSHAKE);
buf.put_u16(record_version); let length_pos = buf.len();
buf.put_u16(0);
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
let hs_length_pos = buf.len();
buf.put_u8(0);
buf.put_u16(0);
buf.put_u16(VERSION_TLS_1_2);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs() as u32;
buf.put_u32(timestamp);
buf.put_slice(&[0u8; 28]);
buf.put_u8(0);
let ciphers = vec![0xc02f, 0xc030];
buf.put_u16((ciphers.len() * 2) as u16);
for cipher in ciphers {
buf.put_u16(cipher);
}
buf.put_u8(1);
buf.put_u8(0);
let total_length = buf.len() - length_pos - 2;
let hs_length = buf.len() - hs_length_pos - 3;
let mut result = buf.to_vec();
result[length_pos] = ((total_length >> 8) & 0xff) as u8;
result[length_pos + 1] = (total_length & 0xff) as u8;
result[hs_length_pos] = ((hs_length >> 16) & 0xff) as u8;
result[hs_length_pos + 1] = ((hs_length >> 8) & 0xff) as u8;
result[hs_length_pos + 2] = (hs_length & 0xff) as u8;
Ok(result)
}
fn build_long_client_hello(&self) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
buf.put_u8(CONTENT_TYPE_HANDSHAKE);
buf.put_u16(VERSION_TLS_1_0);
let length_pos = buf.len();
buf.put_u16(0);
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
let hs_length_pos = buf.len();
buf.put_u8(0);
buf.put_u16(0);
buf.put_u16(VERSION_TLS_1_2);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs() as u32;
buf.put_u32(timestamp);
buf.put_slice(&[0u8; 28]);
buf.put_u8(0);
let ciphers = vec![0xc02f, 0xc030, 0x009c, 0x009d];
buf.put_u16((ciphers.len() * 2) as u16);
for cipher in ciphers {
buf.put_u16(cipher);
}
buf.put_u8(1);
buf.put_u8(0);
let mut extensions = BytesMut::new();
self.add_sni_extension(&mut extensions, &self.target.hostname);
let current_size = buf.len() + 2 + extensions.len(); let padding_needed = if current_size < 300 {
300 - current_size
} else {
100
};
extensions.put_u16(0x0015); extensions.put_u16(padding_needed as u16); extensions.put_slice(&vec![0u8; padding_needed]);
buf.put_u16(extensions.len() as u16);
buf.put_slice(&extensions);
let total_length = buf.len() - length_pos - 2;
let hs_length = buf.len() - hs_length_pos - 3;
let mut result = buf.to_vec();
result[length_pos] = ((total_length >> 8) & 0xff) as u8;
result[length_pos + 1] = (total_length & 0xff) as u8;
result[hs_length_pos] = ((hs_length >> 16) & 0xff) as u8;
result[hs_length_pos + 1] = ((hs_length >> 8) & 0xff) as u8;
result[hs_length_pos + 2] = (hs_length & 0xff) as u8;
Ok(result)
}
fn build_invalid_sni_client_hello(&self) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
buf.put_u8(CONTENT_TYPE_HANDSHAKE);
buf.put_u16(VERSION_TLS_1_0);
let length_pos = buf.len();
buf.put_u16(0);
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
let hs_length_pos = buf.len();
buf.put_u8(0);
buf.put_u16(0);
buf.put_u16(VERSION_TLS_1_2);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs() as u32;
buf.put_u32(timestamp);
buf.put_slice(&[0u8; 28]);
buf.put_u8(0);
let ciphers = vec![0xc02f, 0xc030];
buf.put_u16((ciphers.len() * 2) as u16);
for cipher in ciphers {
buf.put_u16(cipher);
}
buf.put_u8(1);
buf.put_u8(0);
let mut extensions = BytesMut::new();
self.add_sni_extension(&mut extensions, "invalid.nonexistent.example.com");
buf.put_u16(extensions.len() as u16);
buf.put_slice(&extensions);
let total_length = buf.len() - length_pos - 2;
let hs_length = buf.len() - hs_length_pos - 3;
let mut result = buf.to_vec();
result[length_pos] = ((total_length >> 8) & 0xff) as u8;
result[length_pos + 1] = (total_length & 0xff) as u8;
result[hs_length_pos] = ((hs_length >> 16) & 0xff) as u8;
result[hs_length_pos + 1] = ((hs_length >> 8) & 0xff) as u8;
result[hs_length_pos + 2] = (hs_length & 0xff) as u8;
Ok(result)
}
fn add_sni_extension(&self, buf: &mut BytesMut, hostname: &str) {
buf.put_u16(EXTENSION_SERVER_NAME);
let ext_data_len = 2 + 1 + 2 + hostname.len(); buf.put_u16(ext_data_len as u16);
let list_len = 1 + 2 + hostname.len();
buf.put_u16(list_len as u16);
buf.put_u8(0); buf.put_u16(hostname.len() as u16);
buf.put_slice(hostname.as_bytes());
}
fn add_supported_groups_extension(&self, buf: &mut BytesMut) {
buf.put_u16(EXTENSION_SUPPORTED_GROUPS);
let curves = vec![
0x0017, 0x0018, 0x0019, ];
buf.put_u16((2 + curves.len() * 2) as u16); buf.put_u16((curves.len() * 2) as u16);
for curve in curves {
buf.put_u16(curve);
}
}
fn add_ec_point_formats_extension(&self, buf: &mut BytesMut) {
buf.put_u16(EXTENSION_EC_POINT_FORMATS);
buf.put_u16(2); buf.put_u8(1); buf.put_u8(0); }
fn add_signature_algorithms_extension(&self, buf: &mut BytesMut) {
buf.put_u16(EXTENSION_SIGNATURE_ALGORITHMS);
let algorithms = vec![
(0x04, 0x01), (0x05, 0x01), (0x06, 0x01), (0x04, 0x03), (0x05, 0x03), ];
buf.put_u16((2 + algorithms.len() * 2) as u16); buf.put_u16((algorithms.len() * 2) as u16);
for (hash, sig) in algorithms {
buf.put_u8(hash);
buf.put_u8(sig);
}
}
async fn send_client_hello(&self, client_hello: &[u8]) -> Result<Vec<u8>> {
use crate::TlsError;
let addr = self.target.socket_addrs()[0];
let stream = timeout(self.connect_timeout, TcpStream::connect(addr))
.await
.map_err(|_| TlsError::ConnectionTimeout {
duration: self.connect_timeout,
addr,
})??;
let (mut reader, mut writer) = tokio::io::split(stream);
writer.write_all(client_hello).await?;
writer.flush().await?;
let mut response = vec![0u8; BUFFER_SIZE_MAX_TLS_RECORD];
match timeout(self.read_timeout, reader.read(&mut response)).await {
Ok(Ok(n)) if n > 0 => {
response.truncate(n);
Ok(response)
}
_ => Err(TlsError::Timeout {
duration: self.read_timeout,
}),
}
}
async fn send_and_read_alert(&self, client_hello: &[u8]) -> Result<Option<u8>> {
match self.send_client_hello(client_hello).await {
Ok(response) => {
if response.len() >= 7
&& response[0] == CONTENT_TYPE_ALERT
&& response[5] == ALERT_LEVEL_FATAL
{
let alert_code = response[6];
Ok(Some(alert_code))
} else {
Ok(None)
}
}
Err(_) => Ok(None),
}
}
async fn extract_dh_prime(&self) -> Result<Option<String>> {
use openssl::ssl::{SslConnector, SslMethod};
let addr = self.target.socket_addrs()[0];
let stream = timeout(self.connect_timeout, TcpStream::connect(addr))
.await
.map_err(|_| anyhow::anyhow!("Connection timeout"))??;
let std_stream = stream.into_std()?;
std_stream.set_nonblocking(false)?;
let mut builder = SslConnector::builder(SslMethod::tls())?;
builder.set_cipher_list("DHE:EDH:!aNULL:!eNULL")?;
let connector = builder.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(ssl_stream) => {
let cipher = ssl_stream.ssl().current_cipher();
if let Some(c) = cipher {
let cipher_name = c.name();
if cipher_name.contains("DHE") {
return Ok(None);
}
}
Ok(None)
}
Err(_) => Ok(None),
}
}
fn load_common_primes() -> Result<Vec<String>> {
let primes_data = include_str!("../../data/common-primes.txt");
let mut primes = Vec::new();
for line in primes_data.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
primes.push(trimmed.to_uppercase());
}
Ok(primes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_common_primes() {
let primes =
IntoleranceTester::load_common_primes().expect("test assertion should succeed");
assert!(!primes.is_empty());
assert!(primes.len() > 10);
}
#[test]
fn test_intolerance_result_default() {
let result = IntoleranceTestResult::default();
assert!(!result.extension_intolerance);
assert!(!result.version_intolerance);
assert!(!result.long_handshake_intolerance);
assert!(!result.incorrect_sni_alerts);
assert!(!result.uses_common_dh_primes);
assert!(result.details.is_empty());
}
#[tokio::test]
async fn test_build_minimal_client_hello() {
let target = Target::with_ips(
"example.com".to_string(),
443,
vec!["93.184.216.34".parse().unwrap()],
)
.unwrap();
let tester = IntoleranceTester::new(target);
let hello = tester
.build_minimal_client_hello()
.expect("test assertion should succeed");
assert_eq!(hello[0], 0x16); assert_eq!(hello[5], 0x01); }
#[tokio::test]
async fn test_build_extended_client_hello() {
let target = Target::with_ips(
"example.com".to_string(),
443,
vec!["93.184.216.34".parse().unwrap()],
)
.unwrap();
let tester = IntoleranceTester::new(target);
let hello = tester
.build_extended_client_hello()
.expect("test assertion should succeed");
assert_eq!(hello[0], 0x16); assert_eq!(hello[5], 0x01);
let minimal = tester
.build_minimal_client_hello()
.expect("test assertion should succeed");
assert!(hello.len() > minimal.len());
}
#[tokio::test]
async fn test_build_long_client_hello() {
let target = Target::with_ips(
"example.com".to_string(),
443,
vec!["93.184.216.34".parse().unwrap()],
)
.unwrap();
let tester = IntoleranceTester::new(target);
let hello = tester
.build_long_client_hello()
.expect("test assertion should succeed");
assert!(hello.len() > 256);
}
}