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::error::{Result, SeerError};
17use crate::validation::{describe_reserved_ip, normalize_domain};
18
19const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct SslReport {
25 pub domain: String,
27 pub chain: Vec<CertDetail>,
29 pub protocol_version: Option<String>,
31 pub san_names: Vec<String>,
33 pub is_valid: bool,
35 pub days_until_expiry: i64,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CertDetail {
42 pub subject: String,
44 pub issuer: String,
46 pub valid_from: DateTime<Utc>,
48 pub valid_until: DateTime<Utc>,
50 pub serial_number: String,
52 pub signature_algorithm: Option<String>,
54 pub is_ca: bool,
56 pub key_type: Option<String>,
58 pub key_bits: Option<u32>,
60}
61
62#[derive(Debug, Clone)]
64pub struct SslChecker;
65
66impl Default for SslChecker {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl SslChecker {
73 pub fn new() -> Self {
75 Self
76 }
77
78 #[instrument(skip(self), fields(domain = %domain))]
90 pub async fn check(&self, domain: &str) -> Result<SslReport> {
91 let domain = normalize_domain(domain)?;
92 let addr = format!("{}:443", domain);
93
94 debug!(domain = %domain, "Checking SSL certificate chain");
95
96 let socket_addrs: Vec<_> = tokio::net::lookup_host(&addr)
98 .await
99 .map_err(|e| {
100 SeerError::SslError(format!(
101 "could not resolve {} to an IP address for SSL inspection \
102 (no A or AAAA record?): {}",
103 domain, e
104 ))
105 })?
106 .collect();
107
108 if socket_addrs.is_empty() {
109 return Err(SeerError::SslError(format!(
110 "{} has no A or AAAA records — nothing to connect to for SSL inspection",
111 domain
112 )));
113 }
114
115 for socket_addr in &socket_addrs {
116 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
117 return Err(SeerError::SslError(format!(
118 "cannot connect to {}: {} — {}",
119 domain,
120 socket_addr.ip(),
121 reason
122 )));
123 }
124 }
125
126 let connector = native_tls::TlsConnector::builder()
128 .danger_accept_invalid_certs(true)
129 .build()
130 .map_err(|e| SeerError::SslError(format!("Failed to create TLS connector: {}", e)))?;
131 let connector = TlsConnector::from(connector);
132
133 let stream =
135 tokio::time::timeout(DEFAULT_TIMEOUT, TcpStream::connect(socket_addrs.as_slice()))
136 .await
137 .map_err(|_| SeerError::Timeout("SSL connection timed out".to_string()))?
138 .map_err(|e| {
139 SeerError::SslError(format!("Failed to connect to {}: {}", addr, e))
140 })?;
141
142 let tls_stream = tokio::time::timeout(DEFAULT_TIMEOUT, connector.connect(&domain, stream))
144 .await
145 .map_err(|_| SeerError::Timeout("TLS handshake timed out".to_string()))?
146 .map_err(|e| SeerError::SslError(format!("TLS handshake failed: {}", e)))?;
147
148 let cert = tls_stream
150 .get_ref()
151 .peer_certificate()
152 .map_err(|e| SeerError::SslError(format!("Failed to get certificate: {}", e)))?
153 .ok_or_else(|| SeerError::SslError("No certificate presented".to_string()))?;
154
155 let der = cert
156 .to_der()
157 .map_err(|e| SeerError::SslError(format!("Failed to encode certificate: {}", e)))?;
158
159 let (_, x509) = X509Certificate::from_der(&der)
161 .map_err(|e| SeerError::SslError(format!("Failed to parse certificate: {}", e)))?;
162
163 let san_names = extract_sans(&x509);
165
166 let leaf_detail = parse_cert_detail(&x509)?;
169
170 let now = Utc::now();
171 let days_until_expiry = (leaf_detail.valid_until - now).num_days();
172 let is_valid = now >= leaf_detail.valid_from && now <= leaf_detail.valid_until;
173
174 Ok(SslReport {
175 domain,
176 chain: vec![leaf_detail],
177 protocol_version: None,
178 san_names,
179 is_valid,
180 days_until_expiry,
181 })
182 }
183}
184
185fn extract_sans(cert: &X509Certificate) -> Vec<String> {
187 let mut sans = Vec::new();
188 if let Ok(Some(ext)) = cert.subject_alternative_name() {
189 for name in &ext.value.general_names {
190 match name {
191 GeneralName::DNSName(dns) => {
192 sans.push(dns.to_string());
193 }
194 GeneralName::IPAddress(ip_bytes) => {
195 let ip_str = match ip_bytes.len() {
197 4 => format!(
198 "{}.{}.{}.{}",
199 ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]
200 ),
201 16 => {
202 let mut parts = Vec::new();
204 for chunk in ip_bytes.chunks(2) {
205 parts.push(format!("{:02x}{:02x}", chunk[0], chunk[1]));
206 }
207 parts.join(":")
208 }
209 _ => format!("{:?}", ip_bytes),
210 };
211 sans.push(ip_str);
212 }
213 _ => {}
214 }
215 }
216 }
217 sans
218}
219
220fn parse_cert_detail(cert: &X509Certificate) -> Result<CertDetail> {
222 let subject = cert.subject().to_string();
223 let issuer = cert.issuer().to_string();
224
225 let valid_from = asn1_time_to_chrono(cert.validity().not_before)?;
226 let valid_until = asn1_time_to_chrono(cert.validity().not_after)?;
227
228 let serial_number = cert.serial.to_str_radix(16);
229
230 let signature_algorithm = oid_to_name(&cert.signature_algorithm.algorithm);
231
232 let is_ca = cert.is_ca();
233
234 let spki = cert.public_key();
236 let (key_type, key_bits) = extract_key_info(spki);
237
238 Ok(CertDetail {
239 subject,
240 issuer,
241 valid_from,
242 valid_until,
243 serial_number,
244 signature_algorithm,
245 is_ca,
246 key_type,
247 key_bits,
248 })
249}
250
251fn extract_key_info(spki: &SubjectPublicKeyInfo) -> (Option<String>, Option<u32>) {
253 use x509_parser::public_key::PublicKey;
254 let oid = &spki.algorithm.algorithm;
255 let key_type = oid_to_key_type(oid);
256 let key_bits = match spki.parsed() {
257 Ok(PublicKey::RSA(rsa)) => Some(rsa.key_size() as u32),
258 Ok(PublicKey::EC(ec)) => Some(ec.key_size() as u32),
259 _ => None,
260 };
261 (key_type, key_bits)
262}
263
264fn oid_to_name(oid: &Oid) -> Option<String> {
266 let oid_str = format!("{}", oid);
267 match oid_str.as_str() {
268 "1.2.840.113549.1.1.11" => Some("SHA-256 with RSA".to_string()),
269 "1.2.840.113549.1.1.12" => Some("SHA-384 with RSA".to_string()),
270 "1.2.840.113549.1.1.13" => Some("SHA-512 with RSA".to_string()),
271 "1.2.840.113549.1.1.5" => Some("SHA-1 with RSA".to_string()),
272 "1.2.840.113549.1.1.14" => Some("SHA-224 with RSA".to_string()),
273 "1.2.840.10045.4.3.2" => Some("ECDSA with SHA-256".to_string()),
274 "1.2.840.10045.4.3.3" => Some("ECDSA with SHA-384".to_string()),
275 "1.2.840.10045.4.3.4" => Some("ECDSA with SHA-512".to_string()),
276 "1.3.101.112" => Some("Ed25519".to_string()),
277 "1.3.101.113" => Some("Ed448".to_string()),
278 _ => Some(oid_str),
279 }
280}
281
282fn oid_to_key_type(oid: &Oid) -> Option<String> {
284 let oid_str = format!("{}", oid);
285 match oid_str.as_str() {
286 "1.2.840.113549.1.1.1" => Some("RSA".to_string()),
287 "1.2.840.10045.2.1" => Some("EC".to_string()),
288 "1.3.101.112" => Some("Ed25519".to_string()),
289 "1.3.101.113" => Some("Ed448".to_string()),
290 _ => Some(oid_str),
291 }
292}
293
294fn asn1_time_to_chrono(time: ASN1Time) -> Result<DateTime<Utc>> {
296 let timestamp = time.timestamp();
297 DateTime::from_timestamp(timestamp, 0)
298 .ok_or_else(|| SeerError::SslError("invalid certificate timestamp".to_string()))
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_ssl_checker_creation() {
307 let checker = SslChecker::new();
308 let _default_checker = SslChecker::default();
309 drop(checker);
311 }
312
313 #[test]
314 fn test_oid_to_name() {
315 let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 11][..]).unwrap();
316 assert_eq!(oid_to_name(&oid), Some("SHA-256 with RSA".to_string()));
317 }
318
319 #[test]
320 fn test_oid_to_key_type() {
321 let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 1][..]).unwrap();
322 assert_eq!(oid_to_key_type(&oid), Some("RSA".to_string()));
323 }
324
325 #[test]
326 fn test_ssl_report_serialization() {
327 let report = SslReport {
328 domain: "example.com".to_string(),
329 chain: vec![CertDetail {
330 subject: "CN=example.com".to_string(),
331 issuer: "CN=R3, O=Let's Encrypt".to_string(),
332 valid_from: Utc::now(),
333 valid_until: Utc::now(),
334 serial_number: "abc123".to_string(),
335 signature_algorithm: Some("SHA-256 with RSA".to_string()),
336 is_ca: false,
337 key_type: Some("RSA".to_string()),
338 key_bits: Some(2048),
339 }],
340 protocol_version: None,
341 san_names: vec!["example.com".to_string(), "*.example.com".to_string()],
342 is_valid: true,
343 days_until_expiry: 90,
344 };
345 let json = serde_json::to_string(&report).unwrap();
346 assert!(json.contains("example.com"));
347 assert!(json.contains("SHA-256 with RSA"));
348 assert!(json.contains("\"is_valid\":true"));
349 }
350}