use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use rustls::{
pki_types::{IpAddr as RustlsIpAddr, ServerName},
ClientConfig, ProtocolVersion, RootCertStore,
};
use rustls_native_certs::load_native_certs;
use serde::Serialize;
use std::{
net::{IpAddr, ToSocketAddrs},
str::FromStr,
sync::Arc,
time::Duration,
};
use tokio::{net::TcpStream, time::timeout};
use tokio_rustls::TlsConnector;
use x509_parser::{extensions::GeneralName, prelude::*};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Serialize, Clone)]
pub struct Info {
pub issuer: String,
pub subject: String,
pub not_before: String,
pub not_after: String,
pub dns_names: Vec<String>,
pub tls_version: String,
}
pub async fn fetch_ssl_info(target: &str) -> Result<Info> {
let mut root_store = RootCertStore::empty();
for cert in load_native_certs().expect("could not load platform certs") {
if let Err(e) = root_store.add(cert) {
eprintln!("Warning: Failed to add native certificate: {e}");
}
}
let tls_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(tls_config));
let addr = format!("{target}:443")
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("could not resolve `{target}`"))?;
let tcp = timeout(CONNECT_TIMEOUT, TcpStream::connect(addr))
.await
.context("TCP connect timed out")??;
let server_name = match IpAddr::from_str(target) {
Ok(ip) => ServerName::IpAddress(RustlsIpAddr::from(ip)),
Err(_) => ServerName::try_from(target.to_string())?,
};
let tls_stream = timeout(
HANDSHAKE_TIMEOUT,
connector.connect(server_name.to_owned(), tcp),
)
.await
.context("TLS handshake timed out")??;
let session = &tls_stream.get_ref().1;
let tls_version = session
.protocol_version()
.map_or("unknown", |v| match v {
ProtocolVersion::TLSv1_3 => "TLS 1.3",
ProtocolVersion::TLSv1_2 => "TLS 1.2",
_ => "unknown",
})
.to_string();
let chain = session
.peer_certificates()
.ok_or_else(|| anyhow::anyhow!("server returned no certificates"))?;
let end_entity = chain
.first()
.ok_or_else(|| anyhow::anyhow!("certificate chain is empty"))?
.as_ref();
let (_, cert) = X509Certificate::from_der(end_entity)
.context("parsing end-entity certificate")?;
let issuer = cert
.issuer()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map_or_else(|| cert.issuer().to_string(), std::borrow::ToOwned::to_owned);
let subject = cert
.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map_or_else(
|| cert.subject().to_string(),
std::borrow::ToOwned::to_owned,
);
let not_before: DateTime<Utc> =
DateTime::from_timestamp(cert.validity().not_before.timestamp(), 0)
.ok_or_else(|| anyhow::anyhow!("invalid not_before timestamp"))?;
let not_after: DateTime<Utc> =
DateTime::from_timestamp(cert.validity().not_after.timestamp(), 0)
.ok_or_else(|| anyhow::anyhow!("invalid not_after timestamp"))?;
let dns_names = cert
.subject_alternative_name()
.ok()
.flatten()
.map(|ext| {
ext
.value
.general_names
.iter()
.filter_map(|gn| match gn {
GeneralName::DNSName(n) => Some((*n).to_owned()),
_ => None,
})
.collect()
})
.unwrap_or_default();
Ok(Info {
issuer,
subject,
not_before: not_before.to_rfc2822(),
not_after: not_after.to_rfc2822(),
dns_names,
tls_version,
})
}