using System.Security.Cryptography.X509Certificates; foreach (string path in args){ using X509Certificate2 cert = X509CertificateLoader.LoadCertificateFromFile(path); // PEM or DER DateTime notAfter = cert.NotAfter.ToUniversalTime(); // NotAfter is in local time int daysLeft = (int)Math.Floor((notAfter - DateTime.UtcNow).TotalDays); Console.WriteLine($"{path}: notAfter {notAfter:O}, {daysLeft} days left");} using System.Net.Security;using System.Net.Sockets;using System.Security.Cryptography.X509Certificates; string host = args[0];int port = int.Parse(args[1]); var options = new SslClientAuthenticationOptions{ TargetHost = host, // SNI, and the name the certificate must match CertificateChainPolicy = new X509ChainPolicy { TrustMode = X509ChainTrustMode.CustomRootTrust, // our root instead of the system store CustomTrustStore = { X509CertificateLoader.LoadCertificateFromFile("root.pem") }, RevocationMode = X509RevocationMode.NoCheck, // a test CA publishes no CRL or OCSP },}; using var tcp = new TcpClient();await tcp.ConnectAsync(host, port);using var tls = new SslStream(tcp.GetStream());await tls.AuthenticateAsClientAsync(options); // throws if the chain or the name does not verifyvar leaf = (X509Certificate2)tls.RemoteCertificate!;Console.WriteLine($"{host}: notAfter {leaf.NotAfter.ToUniversalTime():O}"); How it works
X509CertificateLoader.LoadCertificateFromFile(.NET 9+) reads PEM and DER;NotAfter.ToUniversalTime()gives the UTC instant, and the difference toDateTime.UtcNowthe days left.SslClientAuthenticationOptions.CertificateChainPolicywithCustomRootTrustvalidates the server against your own root instead of the system store;TargetHostsends SNI and is the name checked.- After
AuthenticateAsClientAsync,RemoteCertificateis the verified leaf; cast it toX509Certificate2forNotAfter.RevocationMode = NoCheckis there only because the test CA publishes no CRL or OCSP; leave revocation checking on for public servers. - The run prints
2035-12-31T23:59:59.0000000Zfor the test leaf, from the file and from the local server.
Gotchas
NotAfteris local time (Kind == Local): withTZ=Asia/Tokyothe run prints2036-01-01 08:59:59for a certificate that expires on 31 December 2035 UTC. CallToUniversalTime()before printing or comparing.- An expired server certificate makes the handshake throw
AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: NotTimeValid— catch it and report the expiry rather than a connection error.