use std::fmt;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, Error, SignatureScheme};
use tokio_rustls::TlsConnector;
use x509_cert::der::Decode;
use x509_cert::Certificate;
#[derive(Debug, Clone, PartialEq)]
pub struct TlsCertInfo {
pub subject: String,
pub issuer: String,
pub sans: Vec<String>,
pub not_after_unix: i64,
pub is_self_signed: bool,
pub cipher_suite: String,
pub protocol_version: String,
}
impl fmt::Display for TlsCertInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"TlsCertInfo(subject: {}, issuer: {}, sans: {:?}, expires: {}, self-signed: {}, cipher: {}, version: {})",
self.subject,
self.issuer,
self.sans,
self.not_after_unix,
self.is_self_signed,
self.cipher_suite,
self.protocol_version,
)
}
}
#[derive(Debug)]
struct AcceptAll;
impl ServerCertVerifier for AcceptAll {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_msg: &[u8],
_cert: &CertificateDer<'_>,
_sig: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_msg: &[u8],
_cert: &CertificateDer<'_>,
_sig: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::RSA_PKCS1_SHA1,
SignatureScheme::ECDSA_SHA1_Legacy,
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::RSA_PKCS1_SHA384,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::RSA_PKCS1_SHA512,
SignatureScheme::ECDSA_NISTP521_SHA512,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::RSA_PSS_SHA384,
SignatureScheme::RSA_PSS_SHA512,
SignatureScheme::ED25519,
SignatureScheme::ED448,
]
}
}
pub async fn probe_tls(
addr: &str,
port: u16,
timeout: Duration,
proxy: Option<&str>,
) -> Option<TlsCertInfo> {
let config = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(AcceptAll))
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(config));
let stream = tokio::time::timeout(timeout, gossan_core::net::connect_tcp(addr, port, proxy))
.await
.ok()?
.ok()?;
let server_name = ServerName::try_from(addr.to_string()).ok()?;
let tls_stream = tokio::time::timeout(timeout, connector.connect(server_name, stream))
.await
.ok()?
.ok()?;
let (_, server_conn) = tls_stream.get_ref();
let cipher_suite = server_conn
.negotiated_cipher_suite()
.map(|s| format!("{:?}", s.suite()))
.unwrap_or_default();
let protocol_version = server_conn
.protocol_version()
.map(|v| format!("{v:?}"))
.unwrap_or_default();
let certs = server_conn.peer_certificates()?;
let der = certs.first()?;
let mut info = parse_cert(der)?;
info.cipher_suite = cipher_suite;
info.protocol_version = protocol_version;
Some(info)
}
fn parse_cert(der: &CertificateDer<'_>) -> Option<TlsCertInfo> {
let cert = Certificate::from_der(der.as_ref()).ok()?;
let tbs = &cert.tbs_certificate;
let subject = tbs.subject.to_string();
let issuer = tbs.issuer.to_string();
let is_self_signed = subject == issuer;
let not_after_unix = {
let st = tbs.validity.not_after.to_system_time();
st.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
};
let mut sans: Vec<String> = Vec::new();
if let Some(exts) = &tbs.extensions {
use x509_cert::ext::pkix::name::GeneralName;
use x509_cert::ext::pkix::SubjectAltName;
for ext in exts.iter() {
if ext.extn_id.to_string() == "2.5.29.17" {
if let Ok(san) = SubjectAltName::from_der(ext.extn_value.as_bytes()) {
for name in &san.0 {
if let GeneralName::DnsName(dns) = name {
let s = dns.as_str().trim_start_matches("*.").to_string();
if !s.is_empty() {
sans.push(s);
}
}
}
}
}
}
}
if sans.is_empty() {
if let Some(cn) = extract_cn(&subject) {
sans.push(cn);
}
}
Some(TlsCertInfo {
subject,
issuer,
sans,
not_after_unix,
is_self_signed,
cipher_suite: String::new(),
protocol_version: String::new(),
})
}
fn extract_cn(rdnseq: &str) -> Option<String> {
for part in rdnseq.split(',') {
let part = part.trim();
if let Some(val) = part.strip_prefix("CN=") {
return Some(val.trim().to_string());
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LegacyTlsResult {
pub supports_tls10: bool,
pub supports_tls11: bool,
}
impl fmt::Display for LegacyTlsResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.supports_tls10 || self.supports_tls11 {
write!(
f,
"LegacyTlsResult(TLS 1.0: {}, TLS 1.1: {})",
if self.supports_tls10 {
"VULNERABLE"
} else {
"no"
},
if self.supports_tls11 {
"deprecated"
} else {
"no"
}
)
} else {
write!(f, "LegacyTlsResult(no legacy protocols)")
}
}
}
pub async fn probe_legacy(
addr: &str,
port: u16,
timeout: Duration,
proxy: Option<&str>,
) -> LegacyTlsResult {
let tls10 = probe_raw_version(addr, port, timeout, [0x03, 0x01], proxy).await;
let tls11 = probe_raw_version(addr, port, timeout, [0x03, 0x02], proxy).await;
LegacyTlsResult {
supports_tls10: tls10,
supports_tls11: tls11,
}
}
async fn probe_raw_version(
addr: &str,
port: u16,
timeout: Duration,
version: [u8; 2],
proxy: Option<&str>,
) -> bool {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let Ok(Ok(mut stream)) =
tokio::time::timeout(timeout, gossan_core::net::connect_tcp(addr, port, proxy)).await
else {
return false;
};
let hello: Vec<u8> = {
let mut h = Vec::with_capacity(49);
h.push(0x16); h.extend_from_slice(&version); h.extend_from_slice(&[0x00, 0x29]);
h.push(0x01); h.extend_from_slice(&[0x00, 0x00, 0x25]);
h.extend_from_slice(&version); h.extend_from_slice(&[
0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1A, 0x1B, 0x1C,
]);
h.push(0x00); h.extend_from_slice(&[0x00, 0x02]); h.extend_from_slice(&[0x00, 0x2F]); h.push(0x01); h.push(0x00); h
};
if tokio::time::timeout(timeout, stream.write_all(&hello))
.await
.ok()
.is_none()
{
return false;
}
let mut header = [0u8; 5];
let Ok(Ok(_)) =
tokio::time::timeout(Duration::from_secs(3), stream.read_exact(&mut header)).await
else {
return false;
};
header[0] == 0x16 && header[1] == 0x03
}
pub fn days_until_expiry(not_after_unix: i64) -> i64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
(not_after_unix - now) / 86_400
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_cn_returns_common_name() {
assert_eq!(
extract_cn("CN=example.com, O=Acme"),
Some("example.com".into())
);
}
#[test]
fn extract_cn_ignores_non_cn_attributes() {
assert_eq!(extract_cn("O=Acme, OU=Security"), None);
}
#[test]
fn days_until_expiry_is_positive_for_future_dates() {
let future = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3 * 86_400;
let days = days_until_expiry(future);
assert!((2..=3).contains(&days));
}
#[test]
fn days_until_expiry_is_negative_for_past_dates() {
let past = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
- 2 * 86_400;
assert!(days_until_expiry(past) <= -1);
}
#[test]
fn accept_all_reports_supported_verify_schemes() {
let schemes = AcceptAll.supported_verify_schemes();
assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256));
assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256));
assert!(schemes.contains(&SignatureScheme::ED25519));
}
}