use crate::Result;
use crate::constants::{
CONTENT_TYPE_HANDSHAKE, DEFAULT_READ_TIMEOUT, EXTENSION_RENEGOTIATION_INFO,
HANDSHAKE_TYPE_CLIENT_HELLO, SHORT_TIMEOUT, VERSION_TLS_1_2, VULNERABILITY_CHECK_BUFFER_SIZE,
};
use crate::utils::network::Target;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::timeout;
pub struct RenegotiationTester<'a> {
target: &'a Target,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RenegotiationSupport {
SecureRenegotiation, InsecureRenegotiation, ClientInitiatedDisabled, NotSupported, }
impl<'a> RenegotiationTester<'a> {
pub fn new(target: &'a Target) -> Self {
Self { target }
}
pub async fn test(&self) -> Result<RenegotiationTestResult> {
let support = self.test_renegotiation_support().await?;
let secure_extension = self.test_secure_renegotiation_extension().await?;
let vulnerable = matches!(support, RenegotiationSupport::InsecureRenegotiation);
let details = match support {
RenegotiationSupport::SecureRenegotiation => {
"Secure renegotiation supported (RFC 5746)".to_string()
}
RenegotiationSupport::InsecureRenegotiation => {
"VULNERABLE: Insecure renegotiation enabled (CVE-2009-3555)".to_string()
}
RenegotiationSupport::ClientInitiatedDisabled => {
"Client-initiated renegotiation disabled (secure configuration)".to_string()
}
RenegotiationSupport::NotSupported => "Renegotiation not supported".to_string(),
};
Ok(RenegotiationTestResult {
support,
secure_extension,
vulnerable,
details,
})
}
async fn test_renegotiation_support(&self) -> Result<RenegotiationSupport> {
use openssl::ssl::{SslConnector, SslMethod};
let addr = self.target.socket_addrs()[0];
match timeout(DEFAULT_READ_TIMEOUT, TcpStream::connect(addr)).await {
Ok(Ok(stream)) => {
let std_stream = stream.into_std()?;
std_stream.set_nonblocking(false)?;
let builder = SslConnector::builder(SslMethod::tls())?;
let connector = builder.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(_ssl_stream) => {
Ok(RenegotiationSupport::SecureRenegotiation)
}
Err(_) => Ok(RenegotiationSupport::NotSupported),
}
}
_ => Ok(RenegotiationSupport::NotSupported),
}
}
async fn test_secure_renegotiation_extension(&self) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
match timeout(DEFAULT_READ_TIMEOUT, TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
let client_hello = self.build_client_hello();
stream.write_all(&client_hello).await?;
let mut buffer = vec![0u8; VULNERABILITY_CHECK_BUFFER_SIZE];
match timeout(SHORT_TIMEOUT, stream.read(&mut buffer)).await {
Ok(Ok(n)) if n > 0 => {
let has_extension = self.has_renegotiation_info_extension(&buffer[..n]);
Ok(has_extension)
}
_ => Ok(false),
}
}
_ => Ok(false),
}
}
fn build_client_hello(&self) -> Vec<u8> {
let mut hello = Vec::new();
hello.push(CONTENT_TYPE_HANDSHAKE);
hello.push((VERSION_TLS_1_2 >> 8) as u8);
hello.push((VERSION_TLS_1_2 & 0xff) as u8);
let len_pos = hello.len();
hello.push(0x00);
hello.push(0x00);
hello.push(HANDSHAKE_TYPE_CLIENT_HELLO);
let hs_len_pos = hello.len();
hello.push(0x00);
hello.push(0x00);
hello.push(0x00);
hello.push((VERSION_TLS_1_2 >> 8) as u8);
hello.push((VERSION_TLS_1_2 & 0xff) as u8);
for i in 0..32 {
hello.push((i * 13) as u8);
}
hello.push(0x00);
hello.push(0x00);
hello.push(0x04);
hello.push(0xc0);
hello.push(0x2f); hello.push(0x00);
hello.push(0x9c);
hello.push(0x01);
hello.push(0x00);
let ext_pos = hello.len();
hello.push(0x00);
hello.push(0x00);
hello.push((EXTENSION_RENEGOTIATION_INFO >> 8) as u8);
hello.push((EXTENSION_RENEGOTIATION_INFO & 0xff) as u8);
hello.push(0x00);
hello.push(0x01); hello.push(0x00);
let ext_len = hello.len() - ext_pos - 2;
hello[ext_pos] = ((ext_len >> 8) & 0xff) as u8;
hello[ext_pos + 1] = (ext_len & 0xff) as u8;
let hs_len = hello.len() - hs_len_pos - 3;
hello[hs_len_pos] = ((hs_len >> 16) & 0xff) as u8;
hello[hs_len_pos + 1] = ((hs_len >> 8) & 0xff) as u8;
hello[hs_len_pos + 2] = (hs_len & 0xff) as u8;
let rec_len = hello.len() - len_pos - 2;
hello[len_pos] = ((rec_len >> 8) & 0xff) as u8;
hello[len_pos + 1] = (rec_len & 0xff) as u8;
hello
}
fn has_renegotiation_info_extension(&self, response: &[u8]) -> bool {
let ext_high = (EXTENSION_RENEGOTIATION_INFO >> 8) as u8;
let ext_low = (EXTENSION_RENEGOTIATION_INFO & 0xff) as u8;
response.windows(2).any(|w| w == [ext_high, ext_low])
}
}
#[derive(Debug, Clone)]
pub struct RenegotiationTestResult {
pub support: RenegotiationSupport,
pub secure_extension: bool,
pub vulnerable: bool,
pub details: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_renegotiation_result() {
let result = RenegotiationTestResult {
support: RenegotiationSupport::SecureRenegotiation,
secure_extension: true,
vulnerable: false,
details: "Test".to_string(),
};
assert!(!result.vulnerable);
assert!(result.secure_extension);
}
#[test]
fn test_client_hello_with_renegotiation_info() {
let target = Target::with_ips(
"example.com".to_string(),
443,
vec!["93.184.216.34".parse().unwrap()],
)
.unwrap();
let tester = RenegotiationTester::new(&target);
let hello = tester.build_client_hello();
assert!(hello.len() > 50);
let has_reneg_info = hello.windows(2).any(|w| w == [0xff, 0x01]);
assert!(has_reneg_info);
}
}