1use std::time::Duration;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use tokio::net::TcpStream;
11use tokio_native_tls::TlsConnector;
12use tracing::{debug, instrument};
13use x509_parser::oid_registry::Oid;
14use x509_parser::prelude::*;
15
16use crate::caa::{self, CaaPolicy};
17use crate::dns::DnsResolver;
18use crate::error::{Result, SeerError};
19use crate::net::resolve_public_host;
20use crate::validation::normalize_domain;
21
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SslReport {
28 pub domain: String,
30 pub chain: Vec<CertDetail>,
32 pub protocol_version: Option<String>,
34 pub san_names: Vec<String>,
36 pub is_valid: bool,
38 pub days_until_expiry: i64,
40 #[serde(skip_serializing_if = "Option::is_none")]
47 pub caa: Option<CaaPolicy>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CertDetail {
53 pub subject: String,
55 pub issuer: String,
57 pub valid_from: DateTime<Utc>,
59 pub valid_until: DateTime<Utc>,
61 pub serial_number: String,
63 pub signature_algorithm: Option<String>,
65 pub is_ca: bool,
67 pub key_type: Option<String>,
69 pub key_bits: Option<u32>,
71}
72
73#[derive(Debug, Clone)]
75pub struct SslChecker {
76 dns_resolver: DnsResolver,
78}
79
80impl Default for SslChecker {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl SslChecker {
87 pub fn new() -> Self {
89 Self {
90 dns_resolver: DnsResolver::new(),
91 }
92 }
93
94 #[instrument(skip(self), fields(domain = %domain))]
106 pub async fn check(&self, domain: &str) -> Result<SslReport> {
107 let domain = normalize_domain(domain)?;
108
109 debug!(domain = %domain, "Checking SSL certificate chain");
110
111 let caa_future = caa::lookup_caa(&self.dns_resolver, &domain);
115
116 let resolve_future = resolve_public_host(&domain, 443);
121
122 let (caa_policy, socket_addrs) = tokio::join!(caa_future, resolve_future);
123 let socket_addrs = socket_addrs.map_err(|e| {
124 SeerError::SslError(format!(
125 "could not resolve {} for SSL inspection: {}",
126 domain, e
127 ))
128 })?;
129
130 let connector = native_tls::TlsConnector::builder()
132 .danger_accept_invalid_certs(true)
133 .build()
134 .map_err(|e| SeerError::SslError(format!("Failed to create TLS connector: {}", e)))?;
135 let connector = TlsConnector::from(connector);
136
137 let stream =
139 tokio::time::timeout(DEFAULT_TIMEOUT, TcpStream::connect(socket_addrs.as_slice()))
140 .await
141 .map_err(|_| SeerError::Timeout("SSL connection timed out".to_string()))?
142 .map_err(|e| {
143 SeerError::SslError(format!("Failed to connect to {}:443: {}", domain, e))
144 })?;
145
146 let tls_stream = tokio::time::timeout(DEFAULT_TIMEOUT, connector.connect(&domain, stream))
148 .await
149 .map_err(|_| SeerError::Timeout("TLS handshake timed out".to_string()))?
150 .map_err(|e| SeerError::SslError(format!("TLS handshake failed: {}", e)))?;
151
152 let cert = tls_stream
154 .get_ref()
155 .peer_certificate()
156 .map_err(|e| SeerError::SslError(format!("Failed to get certificate: {}", e)))?
157 .ok_or_else(|| SeerError::SslError("No certificate presented".to_string()))?;
158
159 let der = cert
160 .to_der()
161 .map_err(|e| SeerError::SslError(format!("Failed to encode certificate: {}", e)))?;
162
163 let (_, x509) = X509Certificate::from_der(&der)
165 .map_err(|e| SeerError::SslError(format!("Failed to parse certificate: {}", e)))?;
166
167 let san_names = extract_sans(&x509);
169
170 let leaf_detail = parse_cert_detail(&x509)?;
173
174 let now = Utc::now();
175 let days_until_expiry = (leaf_detail.valid_until - now).num_days();
176 let is_valid = now >= leaf_detail.valid_from && now <= leaf_detail.valid_until;
177
178 let mut caa_policy = caa_policy;
181 caa_policy.issuer_match = Some(caa::classify_issuer(&leaf_detail.issuer, &caa_policy));
182
183 Ok(SslReport {
184 domain,
185 chain: vec![leaf_detail],
186 protocol_version: None,
187 san_names,
188 is_valid,
189 days_until_expiry,
190 caa: Some(caa_policy),
191 })
192 }
193}
194
195fn extract_sans(cert: &X509Certificate) -> Vec<String> {
197 let mut sans = Vec::new();
198 if let Ok(Some(ext)) = cert.subject_alternative_name() {
199 for name in &ext.value.general_names {
200 match name {
201 GeneralName::DNSName(dns) => {
202 sans.push(dns.to_string());
203 }
204 GeneralName::IPAddress(ip_bytes) => {
205 let ip_str = match ip_bytes.len() {
207 4 => format!(
208 "{}.{}.{}.{}",
209 ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]
210 ),
211 16 => {
212 let mut parts = Vec::new();
214 for chunk in ip_bytes.chunks(2) {
215 parts.push(format!("{:02x}{:02x}", chunk[0], chunk[1]));
216 }
217 parts.join(":")
218 }
219 _ => format!("{:?}", ip_bytes),
220 };
221 sans.push(ip_str);
222 }
223 _ => {}
224 }
225 }
226 }
227 sans
228}
229
230fn parse_cert_detail(cert: &X509Certificate) -> Result<CertDetail> {
232 let subject = cert.subject().to_string();
233 let issuer = cert.issuer().to_string();
234
235 let valid_from = asn1_time_to_chrono(cert.validity().not_before)?;
236 let valid_until = asn1_time_to_chrono(cert.validity().not_after)?;
237
238 let serial_number = cert.serial.to_str_radix(16);
239
240 let signature_algorithm = oid_to_name(&cert.signature_algorithm.algorithm);
241
242 let is_ca = cert.is_ca();
243
244 let spki = cert.public_key();
246 let (key_type, key_bits) = extract_key_info(spki);
247
248 Ok(CertDetail {
249 subject,
250 issuer,
251 valid_from,
252 valid_until,
253 serial_number,
254 signature_algorithm,
255 is_ca,
256 key_type,
257 key_bits,
258 })
259}
260
261fn extract_key_info(spki: &SubjectPublicKeyInfo) -> (Option<String>, Option<u32>) {
263 use x509_parser::public_key::PublicKey;
264 let oid = &spki.algorithm.algorithm;
265 let key_type = oid_to_key_type(oid);
266 let key_bits = match spki.parsed() {
267 Ok(PublicKey::RSA(rsa)) => Some(rsa.key_size() as u32),
268 Ok(PublicKey::EC(ec)) => Some(ec.key_size() as u32),
269 _ => None,
270 };
271 (key_type, key_bits)
272}
273
274fn oid_to_name(oid: &Oid) -> Option<String> {
276 let oid_str = format!("{}", oid);
277 match oid_str.as_str() {
278 "1.2.840.113549.1.1.11" => Some("SHA-256 with RSA".to_string()),
279 "1.2.840.113549.1.1.12" => Some("SHA-384 with RSA".to_string()),
280 "1.2.840.113549.1.1.13" => Some("SHA-512 with RSA".to_string()),
281 "1.2.840.113549.1.1.5" => Some("SHA-1 with RSA".to_string()),
282 "1.2.840.113549.1.1.14" => Some("SHA-224 with RSA".to_string()),
283 "1.2.840.10045.4.3.2" => Some("ECDSA with SHA-256".to_string()),
284 "1.2.840.10045.4.3.3" => Some("ECDSA with SHA-384".to_string()),
285 "1.2.840.10045.4.3.4" => Some("ECDSA with SHA-512".to_string()),
286 "1.3.101.112" => Some("Ed25519".to_string()),
287 "1.3.101.113" => Some("Ed448".to_string()),
288 _ => Some(oid_str),
289 }
290}
291
292fn oid_to_key_type(oid: &Oid) -> Option<String> {
294 let oid_str = format!("{}", oid);
295 match oid_str.as_str() {
296 "1.2.840.113549.1.1.1" => Some("RSA".to_string()),
297 "1.2.840.10045.2.1" => Some("EC".to_string()),
298 "1.3.101.112" => Some("Ed25519".to_string()),
299 "1.3.101.113" => Some("Ed448".to_string()),
300 _ => Some(oid_str),
301 }
302}
303
304fn asn1_time_to_chrono(time: ASN1Time) -> Result<DateTime<Utc>> {
306 let timestamp = time.timestamp();
307 DateTime::from_timestamp(timestamp, 0)
308 .ok_or_else(|| SeerError::SslError("invalid certificate timestamp".to_string()))
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_ssl_checker_creation() {
317 let _checker = SslChecker::new();
318 let _default_checker = SslChecker::default();
319 }
320
321 #[test]
322 fn test_oid_to_name() {
323 let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 11][..]).unwrap();
324 assert_eq!(oid_to_name(&oid), Some("SHA-256 with RSA".to_string()));
325 }
326
327 #[test]
328 fn test_oid_to_key_type() {
329 let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 1][..]).unwrap();
330 assert_eq!(oid_to_key_type(&oid), Some("RSA".to_string()));
331 }
332
333 #[tokio::test]
339 #[ignore = "requires network — performs a real TLS handshake"]
340 async fn check_live_example_com_succeeds() {
341 let report = SslChecker::new().check("example.com").await.unwrap();
342 assert_eq!(report.domain, "example.com");
343 assert!(!report.chain.is_empty(), "expected at least a leaf cert");
344 assert!(
345 report.is_valid,
346 "example.com's leaf cert should be currently valid"
347 );
348 }
349
350 #[test]
351 fn test_ssl_report_serialization() {
352 let report = SslReport {
353 domain: "example.com".to_string(),
354 chain: vec![CertDetail {
355 subject: "CN=example.com".to_string(),
356 issuer: "CN=R3, O=Let's Encrypt".to_string(),
357 valid_from: Utc::now(),
358 valid_until: Utc::now(),
359 serial_number: "abc123".to_string(),
360 signature_algorithm: Some("SHA-256 with RSA".to_string()),
361 is_ca: false,
362 key_type: Some("RSA".to_string()),
363 key_bits: Some(2048),
364 }],
365 protocol_version: None,
366 san_names: vec!["example.com".to_string(), "*.example.com".to_string()],
367 is_valid: true,
368 days_until_expiry: 90,
369 caa: None,
370 };
371 let json = serde_json::to_string(&report).unwrap();
372 assert!(json.contains("example.com"));
373 assert!(json.contains("SHA-256 with RSA"));
374 assert!(json.contains("\"is_valid\":true"));
375 }
376}