1use std::error::Error;
2use std::net::TcpStream;
3use std::sync::Arc;
4
5use rustls::pki_types::{CertificateDer, ServerName, pem::PemObject};
6use rustls::{ClientConfig, ClientConnection, RootCertStore};
7use x509_parser::prelude::*;
8
9/// notAfter of the first certificate in a PEM file.
10fn file_not_after(path: &str) -> Result<ASN1Time, Box<dyn Error>> {
11 let der = CertificateDer::from_pem_file(path)?;
12 let (_, cert) = X509Certificate::from_der(&der)?;
13 Ok(cert.validity().not_after)
14}
15
16/// notAfter of the leaf a server presents, once rustls has verified the chain and the name.
17fn server_not_after(host: &str, port: u16, roots: RootCertStore) -> Result<ASN1Time, Box<dyn Error>> {
18 let config = ClientConfig::builder().with_root_certificates(roots).with_no_client_auth();
19 let mut tls = ClientConnection::new(Arc::new(config), ServerName::try_from(host.to_owned())?)?;
20 let mut tcp = TcpStream::connect((host, port))?;
21 while tls.is_handshaking() {
22 tls.complete_io(&mut tcp)?; // returns the verification error, if any
23 }
24 let leaf = tls.peer_certificates().and_then(|chain| chain.first()).ok_or("no certificate")?;
25 let (_, cert) = X509Certificate::from_der(leaf)?;
26 Ok(cert.validity().not_after)
27}
28
29fn main() -> Result<(), Box<dyn Error>> {
30 for path in ["leaf.pem", "expired.pem"] {
31 let not_after = file_not_after(path)?;
32 let days = (not_after.timestamp() - ASN1Time::now().timestamp()).div_euclid(86_400);
33 println!("{path}: notAfter {not_after}, {days} days left");
34 }
35
36 let mut roots = RootCertStore::empty(); // only our test root; webpki-roots has the public ones
37 roots.add(CertificateDer::from_pem_file("root.pem")?)?;
38 println!("api.example.com: notAfter {}", server_not_after("api.example.com", 8443, roots.clone())?);
39 if let Err(error) = server_not_after("expired.example.com", 9443, roots) {
40 println!("expired.example.com: {error}");
41 }
42 Ok(())
43}
44