1use std::time::Duration;
12
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use tracing::{debug, instrument};
16use x509_parser::oid_registry::Oid;
17use x509_parser::prelude::*;
18
19use crate::caa::{self, CaaPolicy};
20use crate::dns::DnsResolver;
21use crate::error::{Result, SeerError};
22use crate::net::resolve_public_host;
23use crate::tls::PresentedChain;
24use crate::validation::normalize_host;
25
26const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
28
29const RSA_MIN_KEY_BITS: u32 = 2048;
31const EC_MIN_KEY_BITS: u32 = 256;
33const CERT_EXPIRING_SOON_DAYS: i64 = 30;
35
36fn derive_cert_warnings(
40 leaf: &CertDetail,
41 is_valid: bool,
42 hostname_verified: bool,
43 days_until_expiry: i64,
44) -> Vec<CertWarning> {
45 let mut warnings = Vec::new();
46 let critical = |kind: CertWarningKind, message: String| CertWarning {
47 severity: CertWarningSeverity::Critical,
48 kind,
49 message,
50 };
51 let warn = |kind: CertWarningKind, message: String| CertWarning {
52 severity: CertWarningSeverity::Warning,
53 kind,
54 message,
55 };
56
57 if let (Some(kt), Some(bits)) = (leaf.key_type.as_deref(), leaf.key_bits) {
59 match kt {
60 "RSA" if bits < RSA_MIN_KEY_BITS => warnings.push(critical(
61 CertWarningKind::WeakKey,
62 format!("RSA key size {bits} is below the {RSA_MIN_KEY_BITS}-bit minimum"),
63 )),
64 "EC" if bits < EC_MIN_KEY_BITS => warnings.push(critical(
65 CertWarningKind::WeakKey,
66 format!("EC key size {bits} is below the {EC_MIN_KEY_BITS}-bit minimum"),
67 )),
68 _ => {}
69 }
70 }
71
72 if let Some(sig) = leaf.signature_algorithm.as_deref() {
74 let lower = sig.to_lowercase();
75 if lower.contains("sha-1") || lower.contains("sha1") || lower.contains("md5") {
76 warnings.push(critical(
77 CertWarningKind::DeprecatedSignature,
78 format!("Certificate uses deprecated signature algorithm: {sig}"),
79 ));
80 }
81 }
82
83 if leaf.subject == leaf.issuer {
85 warnings.push(warn(
86 CertWarningKind::SelfSigned,
87 "Certificate is self-signed (issuer equals subject)".to_string(),
88 ));
89 }
90
91 if leaf.is_ca {
93 warnings.push(warn(
94 CertWarningKind::CaLeaf,
95 "Leaf certificate is marked as a CA certificate".to_string(),
96 ));
97 }
98
99 if days_until_expiry < 0 {
101 warnings.push(critical(
102 CertWarningKind::Expired,
103 format!("Certificate expired {} day(s) ago", -days_until_expiry),
104 ));
105 } else if !is_valid {
106 warnings.push(critical(
108 CertWarningKind::NotYetValid,
109 "Certificate is not yet valid (notBefore is in the future)".to_string(),
110 ));
111 } else if days_until_expiry <= CERT_EXPIRING_SOON_DAYS {
112 warnings.push(warn(
113 CertWarningKind::ExpiringSoon,
114 format!("Certificate expires in {days_until_expiry} day(s)"),
115 ));
116 }
117
118 if !hostname_verified {
120 warnings.push(critical(
121 CertWarningKind::HostnameMismatch,
122 "Certificate does not match the requested hostname".to_string(),
123 ));
124 }
125
126 warnings
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct SslReport {
132 pub domain: String,
134 pub chain: Vec<CertDetail>,
137 pub protocol_version: Option<String>,
139 pub san_names: Vec<String>,
141 pub is_valid: bool,
151 #[serde(default)]
159 pub hostname_verified: bool,
160 pub days_until_expiry: i64,
162 #[serde(skip_serializing_if = "Option::is_none")]
169 pub caa: Option<CaaPolicy>,
170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub warnings: Vec<CertWarning>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "kebab-case")]
184pub enum CertWarningSeverity {
185 Critical,
188 Warning,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "kebab-case")]
197pub enum CertWarningKind {
198 WeakKey,
200 DeprecatedSignature,
202 SelfSigned,
204 CaLeaf,
206 Expired,
208 NotYetValid,
210 ExpiringSoon,
212 HostnameMismatch,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct CertWarning {
219 pub severity: CertWarningSeverity,
221 pub kind: CertWarningKind,
223 pub message: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct CertDetail {
230 pub subject: String,
232 pub issuer: String,
234 pub valid_from: DateTime<Utc>,
236 pub valid_until: DateTime<Utc>,
238 pub serial_number: String,
240 pub signature_algorithm: Option<String>,
242 pub is_ca: bool,
244 pub key_type: Option<String>,
246 pub key_bits: Option<u32>,
248}
249
250#[derive(Debug, Clone)]
252pub struct SslChecker {
253 dns_resolver: DnsResolver,
255 timeout: Duration,
257}
258
259impl Default for SslChecker {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264
265impl SslChecker {
266 pub fn new() -> Self {
268 Self {
269 dns_resolver: DnsResolver::new(),
270 timeout: DEFAULT_TIMEOUT,
271 }
272 }
273
274 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
284 Self {
285 dns_resolver: DnsResolver::from_config(config),
286 timeout: config.http_timeout(),
287 }
288 }
289
290 pub fn with_timeout(mut self, timeout: Duration) -> Self {
293 self.timeout = timeout;
294 self.dns_resolver = self.dns_resolver.with_timeout(timeout);
295 self
296 }
297
298 #[instrument(skip(self), fields(domain = %domain))]
310 pub async fn check(&self, domain: &str) -> Result<SslReport> {
311 let domain = normalize_host(domain)?;
314
315 debug!(domain = %domain, "Checking SSL certificate chain");
316
317 let caa_future = caa::lookup_caa(&self.dns_resolver, &domain, None);
321
322 let resolve_future = resolve_public_host(&domain, 443);
327
328 let (caa_policy, socket_addrs) = tokio::join!(caa_future, resolve_future);
329 let caa_policy = caa_policy.unwrap_or_else(|_| CaaPolicy::empty());
332 let socket_addrs = socket_addrs.map_err(|e| {
333 SeerError::SslError(format!(
334 "could not resolve {} for SSL inspection: {}",
335 domain, e
336 ))
337 })?;
338
339 let presented =
343 crate::tls::inspect(&domain, &socket_addrs, self.timeout, SeerError::SslError).await?;
344
345 build_report(domain, presented, caa_policy)
346 }
347}
348
349fn build_report(
352 domain: String,
353 presented: PresentedChain,
354 mut caa_policy: CaaPolicy,
355) -> Result<SslReport> {
356 let (_, x509) = X509Certificate::from_der(&presented.leaf)
358 .map_err(|e| SeerError::SslError(format!("Failed to parse certificate: {}", e)))?;
359
360 let san_names = extract_sans(&x509);
362 let leaf_detail = parse_cert_detail(&x509)?;
363
364 let now = Utc::now();
365 let days_until_expiry = crate::dates::days_until(leaf_detail.valid_until, now);
366 let is_valid = now >= leaf_detail.valid_from && now <= leaf_detail.valid_until;
367
368 let hostname_verified = crate::tls::cert_matches_host(&x509, &domain);
372
373 let wildcard = crate::tls::cert_names_wildcard(&x509);
377 caa_policy.issuer_match = Some(caa::classify_issuer(
378 &leaf_detail.issuer,
379 &caa_policy,
380 wildcard,
381 ));
382
383 let warnings =
385 derive_cert_warnings(&leaf_detail, is_valid, hostname_verified, days_until_expiry);
386
387 let mut chain = vec![leaf_detail];
391 chain.extend(presented.intermediates.iter().filter_map(|der| {
392 let detail = X509Certificate::from_der(der)
393 .ok()
394 .and_then(|(_, cert)| parse_cert_detail(&cert).ok());
395 if detail.is_none() {
396 debug!("skipping unparseable certificate in the presented chain");
397 }
398 detail
399 }));
400
401 Ok(SslReport {
402 domain,
403 chain,
404 protocol_version: presented.protocol,
405 san_names,
406 is_valid,
407 hostname_verified,
408 days_until_expiry,
409 caa: Some(caa_policy),
410 warnings,
411 })
412}
413
414fn extract_sans(cert: &X509Certificate) -> Vec<String> {
416 let mut sans = Vec::new();
417 if let Ok(Some(ext)) = cert.subject_alternative_name() {
418 for name in &ext.value.general_names {
419 match name {
420 GeneralName::DNSName(dns) => {
421 sans.push(dns.to_string());
422 }
423 GeneralName::IPAddress(ip_bytes) => {
424 sans.push(format_ip_san(ip_bytes));
425 }
426 _ => {}
427 }
428 }
429 }
430 sans
431}
432
433fn format_ip_san(ip_bytes: &[u8]) -> String {
439 crate::tls::san_ip(ip_bytes).map_or_else(|| format!("{:?}", ip_bytes), |ip| ip.to_string())
440}
441
442fn parse_cert_detail(cert: &X509Certificate) -> Result<CertDetail> {
444 let subject = cert.subject().to_string();
445 let issuer = cert.issuer().to_string();
446
447 let (valid_from, valid_until) = crate::tls::validity_window(cert)
448 .ok_or_else(|| SeerError::SslError("invalid certificate timestamp".to_string()))?;
449
450 let serial_number = cert.serial.to_str_radix(16);
451
452 let signature_algorithm = Some(oid_to_name(&cert.signature_algorithm.algorithm));
453
454 let is_ca = cert.is_ca();
455
456 let spki = cert.public_key();
458 let (key_type, key_bits) = extract_key_info(spki);
459
460 Ok(CertDetail {
461 subject,
462 issuer,
463 valid_from,
464 valid_until,
465 serial_number,
466 signature_algorithm,
467 is_ca,
468 key_type,
469 key_bits,
470 })
471}
472
473pub(crate) fn extract_key_info(spki: &SubjectPublicKeyInfo) -> (Option<String>, Option<u32>) {
475 use x509_parser::public_key::PublicKey;
476 let oid = &spki.algorithm.algorithm;
477 let key_type = Some(oid_to_key_type(oid));
478 let key_bits = match spki.parsed() {
479 Ok(PublicKey::RSA(rsa)) => Some(rsa.key_size() as u32),
480 Ok(PublicKey::EC(ec)) => Some(ec.key_size() as u32),
481 _ => None,
482 };
483 (key_type, key_bits)
484}
485
486fn oid_to_name(oid: &Oid) -> String {
489 let name = match oid.to_id_string().as_str() {
490 "1.2.840.113549.1.1.11" => "SHA-256 with RSA",
491 "1.2.840.113549.1.1.12" => "SHA-384 with RSA",
492 "1.2.840.113549.1.1.13" => "SHA-512 with RSA",
493 "1.2.840.113549.1.1.5" => "SHA-1 with RSA",
494 "1.2.840.113549.1.1.4" => "MD5 with RSA",
497 "1.2.840.10045.4.1" => "ECDSA with SHA-1",
498 "1.2.840.10040.4.3" => "DSA with SHA-1",
499 "1.2.840.113549.1.1.14" => "SHA-224 with RSA",
500 "1.2.840.10045.4.3.2" => "ECDSA with SHA-256",
501 "1.2.840.10045.4.3.3" => "ECDSA with SHA-384",
502 "1.2.840.10045.4.3.4" => "ECDSA with SHA-512",
503 "1.3.101.112" => "Ed25519",
504 "1.3.101.113" => "Ed448",
505 other => return other.to_string(),
506 };
507 name.to_string()
508}
509
510fn oid_to_key_type(oid: &Oid) -> String {
513 let name = match oid.to_id_string().as_str() {
514 "1.2.840.113549.1.1.1" => "RSA",
515 "1.2.840.10045.2.1" => "EC",
516 "1.3.101.112" => "Ed25519",
517 "1.3.101.113" => "Ed448",
518 other => return other.to_string(),
519 };
520 name.to_string()
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_ssl_checker_creation() {
529 let _checker = SslChecker::new();
530 let _default_checker = SslChecker::default();
531 }
532
533 #[tokio::test]
536 async fn with_timeout_keeps_the_configured_resolver() {
537 use crate::dns::test_support::{mock_dns_resolver_default, spawn_mock_dns, MockMode};
538
539 let port = spawn_mock_dns(MockMode::Zone).await;
541 let checker = SslChecker {
542 dns_resolver: mock_dns_resolver_default(port),
543 ..SslChecker::new()
544 }
545 .with_timeout(Duration::from_secs(3));
546 assert_eq!(checker.dns_resolver.timeout(), Duration::from_secs(3));
547 let policy = caa::lookup_caa(&checker.dns_resolver, "seer.test", None)
548 .await
549 .unwrap();
550 assert!(policy.has_policy, "the configured resolver must be kept");
551 }
552
553 #[test]
554 fn from_config_applies_http_timeout() {
555 let mut config = crate::config::SeerConfig::default();
556 config.timeouts.http_secs = 77;
557 let checker = SslChecker::from_config(&config);
558 assert_eq!(checker.timeout, Duration::from_secs(77));
559 }
560
561 #[test]
562 fn test_oid_to_name() {
563 let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 11][..]).unwrap();
564 assert_eq!(oid_to_name(&oid), "SHA-256 with RSA");
565 }
566
567 #[test]
568 fn test_oid_to_key_type() {
569 let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 1][..]).unwrap();
570 assert_eq!(oid_to_key_type(&oid), "RSA");
571 }
572
573 #[tokio::test]
579 #[ignore = "requires network — performs a real TLS handshake"]
580 async fn check_live_example_com_succeeds() {
581 let report = SslChecker::new().check("example.com").await.unwrap();
582 assert_eq!(report.domain, "example.com");
583 assert!(!report.chain.is_empty(), "expected at least a leaf cert");
584 assert!(
585 report.is_valid,
586 "example.com's leaf cert should be currently valid"
587 );
588 }
589
590 #[tokio::test]
593 async fn report_carries_the_presented_chain_and_protocol() {
594 use crate::tls::test_support::{serve_once, CA_DER, LEAF_DER};
595
596 let addr = serve_once(&[LEAF_DER, CA_DER], &[&rustls::version::TLS12]).await;
597 let presented = crate::tls::inspect(
598 "chain.test",
599 &[addr],
600 Duration::from_secs(5),
601 SeerError::SslError,
602 )
603 .await
604 .unwrap();
605 let report = build_report("chain.test".to_string(), presented, CaaPolicy::empty()).unwrap();
606
607 let subjects: Vec<&str> = report.chain.iter().map(|c| c.subject.as_str()).collect();
608 assert_eq!(subjects, ["CN=chain.test", "CN=Seer Test CA"]);
609 assert!(report.chain[1].is_ca);
610 assert_eq!(report.protocol_version.as_deref(), Some("TLSv1.2"));
611 assert!(report.hostname_verified);
612 }
613
614 #[tokio::test]
617 async fn an_rsa_1024_leaf_is_inspected_and_flagged() {
618 use crate::tls::test_support::{serve_once, RSA_1024_LEAF};
619
620 let addr = serve_once(&[RSA_1024_LEAF], rustls::DEFAULT_VERSIONS).await;
621 let presented = crate::tls::inspect(
622 "weak-rsa.test",
623 &[addr],
624 Duration::from_secs(5),
625 SeerError::SslError,
626 )
627 .await
628 .unwrap();
629 let report =
630 build_report("weak-rsa.test".to_string(), presented, CaaPolicy::empty()).unwrap();
631
632 assert_eq!(report.chain[0].key_type.as_deref(), Some("RSA"));
633 assert_eq!(report.chain[0].key_bits, Some(1024));
634 assert!(
635 report
636 .warnings
637 .iter()
638 .any(|w| w.severity == CertWarningSeverity::Critical
639 && w.message == "RSA key size 1024 is below the 2048-bit minimum"),
640 "got {:?}",
641 report.warnings
642 );
643 }
644
645 #[test]
646 fn test_ssl_report_serialization() {
647 let report = SslReport {
648 domain: "example.com".to_string(),
649 chain: vec![CertDetail {
650 subject: "CN=example.com".to_string(),
651 issuer: "CN=R3, O=Let's Encrypt".to_string(),
652 valid_from: Utc::now(),
653 valid_until: Utc::now(),
654 serial_number: "abc123".to_string(),
655 signature_algorithm: Some("SHA-256 with RSA".to_string()),
656 is_ca: false,
657 key_type: Some("RSA".to_string()),
658 key_bits: Some(2048),
659 }],
660 protocol_version: None,
661 san_names: vec!["example.com".to_string(), "*.example.com".to_string()],
662 is_valid: true,
663 hostname_verified: true,
664 days_until_expiry: 90,
665 caa: None,
666 warnings: vec![],
667 };
668 let json = serde_json::to_string(&report).unwrap();
669 assert!(json.contains("example.com"));
670 assert!(json.contains("SHA-256 with RSA"));
671 assert!(json.contains("\"is_valid\":true"));
672 assert!(json.contains("\"hostname_verified\":true"));
673 }
674
675 #[test]
676 fn format_ip_san_renders_canonical_addresses() {
677 assert_eq!(format_ip_san(&[192, 168, 0, 1]), "192.168.0.1");
679 let mut loopback = [0u8; 16];
681 loopback[15] = 1;
682 assert_eq!(format_ip_san(&loopback), "::1");
683 let mut addr = [0u8; 16];
686 addr[0] = 0x20;
687 addr[1] = 0x01;
688 addr[2] = 0x0d;
689 addr[3] = 0xb8;
690 addr[15] = 1;
691 assert_eq!(format_ip_san(&addr), "2001:db8::1");
692 }
693
694 fn sample_leaf() -> CertDetail {
695 CertDetail {
696 subject: "CN=example.com".to_string(),
697 issuer: "CN=R3, O=Let's Encrypt".to_string(),
698 valid_from: Utc::now(),
699 valid_until: Utc::now(),
700 serial_number: "abc123".to_string(),
701 signature_algorithm: Some("SHA-256 with RSA".to_string()),
702 is_ca: false,
703 key_type: Some("RSA".to_string()),
704 key_bits: Some(2048),
705 }
706 }
707
708 #[test]
709 fn derive_cert_warnings_clean_cert_has_none() {
710 let w = derive_cert_warnings(&sample_leaf(), true, true, 90);
711 assert!(w.is_empty(), "clean cert should have no warnings: {:?}", w);
712 }
713
714 #[test]
715 fn derive_cert_warnings_flags_weak_rsa_key() {
716 let mut leaf = sample_leaf();
717 leaf.key_bits = Some(1024);
718 let w = derive_cert_warnings(&leaf, true, true, 90);
719 assert!(w.iter().any(|x| x.message.contains("RSA key size 1024")
720 && x.severity == CertWarningSeverity::Critical));
721 }
722
723 #[test]
724 fn derive_cert_warnings_flags_sha1_signature() {
725 let mut leaf = sample_leaf();
726 leaf.signature_algorithm = Some("SHA-1 with RSA".to_string());
727 let w = derive_cert_warnings(&leaf, true, true, 90);
728 assert!(w
729 .iter()
730 .any(|x| x.message.to_lowercase().contains("deprecated signature")));
731 }
732
733 #[test]
734 fn derive_cert_warnings_flags_self_signed_expired_and_mismatch() {
735 let mut leaf = sample_leaf();
736 leaf.issuer = leaf.subject.clone();
737 let w = derive_cert_warnings(&leaf, false, false, -3);
738 assert!(w.iter().any(|x| x.message.contains("self-signed")));
739 assert!(w.iter().any(|x| x.message.contains("expired 3 day(s) ago")));
740 assert!(w.iter().any(|x| x.message.contains("does not match")));
741 }
742
743 #[test]
744 fn derive_cert_warnings_flags_expiring_soon_as_warning() {
745 let w = derive_cert_warnings(&sample_leaf(), true, true, 10);
746 assert_eq!(w.len(), 1);
747 assert_eq!(w[0].severity, CertWarningSeverity::Warning);
748 assert!(w[0].message.contains("expires in 10 day(s)"));
749 }
750
751 #[test]
752 fn a_just_expired_cert_is_reported_expired_not_not_yet_valid() {
753 let now = Utc::now();
754 let days = crate::dates::days_until(now - chrono::Duration::hours(5), now);
755 let w = derive_cert_warnings(&sample_leaf(), false, true, days);
756 assert!(
757 w.iter().any(|x| x.message.contains("expired 1 day(s) ago")),
758 "a just-expired cert must be reported expired: {w:?}"
759 );
760 assert!(!w.iter().any(|x| x.message.contains("not yet valid")));
761 }
762
763 #[test]
764 fn deprecated_signature_oids_are_named_and_flagged() {
765 for (arcs, name) in [
766 (&[1u64, 2, 840, 113549, 1, 1, 4][..], "MD5 with RSA"),
767 (&[1u64, 2, 840, 10045, 4, 1][..], "ECDSA with SHA-1"),
768 (&[1u64, 2, 840, 10040, 4, 3][..], "DSA with SHA-1"),
769 ] {
770 let oid = Oid::from(arcs).unwrap();
771 assert_eq!(oid_to_name(&oid), name);
772 let mut leaf = sample_leaf();
773 leaf.signature_algorithm = Some(oid_to_name(&oid));
774 let w = derive_cert_warnings(&leaf, true, true, 90);
775 assert!(
776 w.iter().any(|x| x.message.contains("deprecated signature")),
777 "{name} must be flagged: {w:?}"
778 );
779 }
780 }
781
782 #[test]
783 fn derive_cert_warnings_flags_ca_marked_leaf() {
784 let mut leaf = sample_leaf();
785 leaf.is_ca = true;
786 let w = derive_cert_warnings(&leaf, true, true, 90);
787 assert!(w.iter().any(|x| x.message.contains("marked as a CA")));
788 }
789
790 #[test]
794 fn ip_only_san_still_falls_back_to_the_cn() {
795 use crate::tls::test_support::{cert, CN_VICTIM_SAN_IP};
796
797 let presented = PresentedChain {
798 leaf: cert(CN_VICTIM_SAN_IP),
799 intermediates: vec![],
800 protocol: None,
801 };
802 let report =
803 build_report("victim.example".to_string(), presented, CaaPolicy::empty()).unwrap();
804 assert!(report.hostname_verified);
805 assert_eq!(report.san_names, ["203.0.113.7"]);
806 }
807
808 #[test]
809 fn warnings_carry_a_structured_kind() {
810 let leaf = CertDetail {
811 subject: "CN=self.test".into(),
812 issuer: "CN=self.test".into(),
813 valid_from: Utc::now() - chrono::Duration::days(400),
814 valid_until: Utc::now() - chrono::Duration::days(1),
815 serial_number: "01".into(),
816 signature_algorithm: Some("SHA-1 with RSA".into()),
817 is_ca: true,
818 key_type: Some("RSA".into()),
819 key_bits: Some(1024),
820 };
821 let kinds: Vec<CertWarningKind> = derive_cert_warnings(&leaf, false, false, -1)
822 .into_iter()
823 .map(|w| w.kind)
824 .collect();
825 assert_eq!(
826 kinds,
827 vec![
828 CertWarningKind::WeakKey,
829 CertWarningKind::DeprecatedSignature,
830 CertWarningKind::SelfSigned,
831 CertWarningKind::CaLeaf,
832 CertWarningKind::Expired,
833 CertWarningKind::HostnameMismatch,
834 ]
835 );
836 }
837
838 #[test]
839 fn not_yet_valid_and_expiring_soon_have_their_own_kinds() {
840 let leaf = CertDetail {
841 subject: "CN=a.test".into(),
842 issuer: "CN=CA".into(),
843 valid_from: Utc::now(),
844 valid_until: Utc::now(),
845 serial_number: "01".into(),
846 signature_algorithm: Some("SHA-256 with RSA".into()),
847 is_ca: false,
848 key_type: Some("RSA".into()),
849 key_bits: Some(2048),
850 };
851 let kind = |valid, days| derive_cert_warnings(&leaf, valid, true, days)[0].kind;
852 assert_eq!(kind(false, 10), CertWarningKind::NotYetValid);
853 assert_eq!(kind(true, 10), CertWarningKind::ExpiringSoon);
854 }
855}