Skip to content
ecdsa.com

Certificate expiry · Rust

How to check SSL certificate expiration date in Rust

To check SSL certificate expiration date in Rust, parse the certificate with x509-parser and read validity().not_after — from a PEM file, or from the leaf a rustls client receives after its handshake. This recipe does both, trusting a private test root, and shows the error rustls returns for an expired server certificate.

Tested with rustls 0.23.46 (Rust 1.99.1, ring provider), x509-parser 0.18.1 (notAfter of files and of the server's leaf), and OpenSSL 3.5.7 (openssl s_server as the local TLS servers).

Cargo.toml — x509-parser, and rustls with the ring provider
[dependencies]
x509-parser = "=0.18.1"
rustls = { version = "=0.23.46", default-features = false, features = ["ring", "std", "tls12"] }
src/main.rs — notAfter from PEM files and from a TLS server
use std::error::Error;
use std::net::TcpStream;
use std::sync::Arc;
use rustls::pki_types::{CertificateDer, ServerName, pem::PemObject};
use rustls::{ClientConfig, ClientConnection, RootCertStore};
use x509_parser::prelude::*;
/// notAfter of the first certificate in a PEM file.
fn file_not_after(path: &str) -> Result<ASN1Time, Box<dyn Error>> {
let der = CertificateDer::from_pem_file(path)?;
let (_, cert) = X509Certificate::from_der(&der)?;
Ok(cert.validity().not_after)
}
/// notAfter of the leaf a server presents, once rustls has verified the chain and the name.
fn server_not_after(host: &str, port: u16, roots: RootCertStore) -> Result<ASN1Time, Box<dyn Error>> {
let config = ClientConfig::builder().with_root_certificates(roots).with_no_client_auth();
let mut tls = ClientConnection::new(Arc::new(config), ServerName::try_from(host.to_owned())?)?;
let mut tcp = TcpStream::connect((host, port))?;
while tls.is_handshaking() {
tls.complete_io(&mut tcp)?; // returns the verification error, if any
}
let leaf = tls.peer_certificates().and_then(|chain| chain.first()).ok_or("no certificate")?;
let (_, cert) = X509Certificate::from_der(leaf)?;
Ok(cert.validity().not_after)
}
fn main() -> Result<(), Box<dyn Error>> {
for path in ["leaf.pem", "expired.pem"] {
let not_after = file_not_after(path)?;
let days = (not_after.timestamp() - ASN1Time::now().timestamp()).div_euclid(86_400);
println!("{path}: notAfter {not_after}, {days} days left");
}
let mut roots = RootCertStore::empty(); // only our test root; webpki-roots has the public ones
roots.add(CertificateDer::from_pem_file("root.pem")?)?;
println!("api.example.com: notAfter {}", server_not_after("api.example.com", 8443, roots.clone())?);
if let Err(error) = server_not_after("expired.example.com", 9443, roots) {
println!("expired.example.com: {error}");
}
Ok(())
}

How it works

  1. CertificateDer::from_pem_file (from rustls::pki_types) reads the first certificate of a PEM file; X509Certificate::from_der parses it, and validity().not_after is an ASN1Time with a timestamp().
  2. RootCertStore::empty() plus add(root) trusts only the test root; for public servers fill it from webpki-roots or the platform verifier.
  3. ClientConnection::new with a ServerName sets SNI and the name to verify; complete_io drives the handshake over a plain TcpStream and returns any verification error.
  4. peer_certificates() returns the chain the server sent, leaf first; the run prints Dec 31 23:59:59 2035 +00:00 for it.

Gotchas

  • rustls refuses the expired server outright: invalid peer certificate: certificate expired: verification time … (UNIX), but certificate is not valid after 1743465599 (… seconds ago). The Unix time in the message is the notAfter, if you need it.
  • ASN1Time prints as Dec 31 23:59:59 2035 +00:00; for RFC 3339 output use to_datetime() with the time crate's formatting.
  • Integer division truncates toward zero; div_euclid(86_400) floors, so an expired certificate reports -568 days rather than -567.

Related recipes