Skip to content
ecdsa.com

Certificate expiry · PHP

How to check SSL certificate expiration date in PHP

To check SSL certificate expiration date in PHP, parse the certificate with openssl_x509_parse and read validTo_time_t — from a file, or from a server through an ssl:// stream with capture_peer_cert. This recipe does both with ext/openssl, trusts a private test root, and shows how an expired server certificate surfaces in PHP.

Tested with PHP 8.5.11 (ext/openssl and ssl:// streams, OpenSSL 3.5.7) and OpenSSL 3.5.7 (openssl s_server as the local TLS servers).

expiry.php — notAfter and days left for a PEM file
<?php
function daysLeft(string $path): int
{
$info = openssl_x509_parse(file_get_contents($path)); // PEM text
if ($info === false) {
throw new RuntimeException("$path: not a certificate: " . openssl_error_string());
}
$notAfter = new DateTimeImmutable('@' . $info['validTo_time_t']); // a Unix timestamp, UTC
$left = intdiv($info['validTo_time_t'] - time(), 86400);
echo "$path: notAfter {$notAfter->format(DATE_ATOM)}, $left days left\n";
return $left;
}
server.php — the expiry of the certificate a TLS server presents
<?php
function serverNotAfter(string $host, int $port, ?string $caFile = null): DateTimeImmutable
{
$ssl = ['capture_peer_cert' => true, 'peer_name' => $host]; // verify_peer is on by default
if ($caFile !== null) {
$ssl['cafile'] = $caFile; // a private CA instead of the system bundle
}
$context = stream_context_create(['ssl' => $ssl]);
$warnings = []; // a failed handshake reports through warnings, the first one names the cause
set_error_handler(function (int $level, string $message) use (&$warnings): bool {
$warnings[] = $message;
return true;
});
try {
$client = stream_socket_client("ssl://$host:$port", $errno, $error, 10, STREAM_CLIENT_CONNECT, $context);
} finally {
restore_error_handler();
}
if ($client === false) {
throw new RuntimeException('TLS connection failed: ' . ($warnings[0] ?? $error));
}
$cert = stream_context_get_params($client)['options']['ssl']['peer_certificate'];
fclose($client);
return new DateTimeImmutable('@' . openssl_x509_parse($cert)['validTo_time_t']);
}

How it works

  1. openssl_x509_parse returns an array; validTo_time_t is the expiry as a Unix timestamp, which new DateTimeImmutable('@' . $ts) turns into a UTC date.
  2. stream_socket_client("ssl://host:port") verifies the peer by default (verify_peer, verify_peer_name); cafile replaces the system bundle and peer_name is the name checked and sent as SNI.
  3. capture_peer_cert keeps the server's certificate in the stream context, where stream_context_get_params(...)['options']['ssl']['peer_certificate'] returns it for openssl_x509_parse.
  4. A failed handshake reports through PHP warnings; serverNotAfter collects them with set_error_handler and throws with the first one, which names the cause.

Gotchas

  • validTo is the raw ASN.1 time (351231235959Z, two-digit year), not something strtotime understands; use validTo_time_t (2082758399).
  • For an expired server certificate PHP only says SSL operation failed with code 1. OpenSSL Error messages: error:0A000086:SSL routines::certificate verify failed — not why. Check the date separately (or with openssl s_client) before concluding the certificate expired.
  • intdiv truncates toward zero: the expired certificate shows -567 days where flooring gives -568. Compare timestamps when the boundary matters.

Related recipes