Skip to content
ecdsa.com

Certificate expiry · .NET

How to check SSL certificate expiration date in C# (.NET)

To check SSL certificate expiration date in C#, read NotAfter from an X509Certificate2 — loaded from a file with X509CertificateLoader, or taken from SslStream.RemoteCertificate after a TLS handshake. This recipe does both on .NET 10, trusts a private test root through X509ChainPolicy, and shows the exception for an expired server certificate.

Tested with .NET 10.0.13 (SDK 10.0.402) (Linux, OpenSSL 3.0.13 backend) and OpenSSL 3.0.13 (openssl s_server as the local TLS servers).

expiry.cs — notAfter and days left for PEM or DER files
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");
}
server.cs — the expiry of the certificate a TLS server presents
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 verify
var leaf = (X509Certificate2)tls.RemoteCertificate!;
Console.WriteLine($"{host}: notAfter {leaf.NotAfter.ToUniversalTime():O}");

How it works

  1. X509CertificateLoader.LoadCertificateFromFile (.NET 9+) reads PEM and DER; NotAfter.ToUniversalTime() gives the UTC instant, and the difference to DateTime.UtcNow the days left.
  2. SslClientAuthenticationOptions.CertificateChainPolicy with CustomRootTrust validates the server against your own root instead of the system store; TargetHost sends SNI and is the name checked.
  3. After AuthenticateAsClientAsync, RemoteCertificate is the verified leaf; cast it to X509Certificate2 for NotAfter. RevocationMode = NoCheck is there only because the test CA publishes no CRL or OCSP; leave revocation checking on for public servers.
  4. The run prints 2035-12-31T23:59:59.0000000Z for the test leaf, from the file and from the local server.

Gotchas

  • NotAfter is local time (Kind == Local): with TZ=Asia/Tokyo the run prints 2036-01-01 08:59:59 for a certificate that expires on 31 December 2035 UTC. Call ToUniversalTime() 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.

Related recipes