Skip to content
ecdsa.com

Certificate expiry · Node.js

How to check SSL certificate expiration date in Node.js

To check SSL certificate expiration date in Node.js, load the file into crypto.X509Certificate and read validToDate, or connect with node:tls and read valid_to from getPeerCertificate(). Both are built in. This recipe checks a certificate file and a local TLS server, and shows what happens — and what you can still read — when the server's certificate has expired.

Tested with Node.js 24.22.0 (node:crypto X509Certificate and node:tls, OpenSSL 3.5.8) and OpenSSL 3.5.7 (openssl s_server as the local TLS servers).

expiry-file.mjs — notAfter and days left for a PEM or DER file
import { X509Certificate } from "node:crypto";
import { readFileSync } from "node:fs";
export function daysLeft(path) {
const cert = new X509Certificate(readFileSync(path)); // PEM or DER
const notAfter = cert.validToDate; // a Date (Node.js 22.10+); validTo is the text form
const left = Math.floor((notAfter.getTime() - Date.now()) / 86_400_000);
console.log(`${path}: notAfter ${notAfter.toISOString()}, ${left} days left`);
return left;
}
expiry-server.mjs — the expiry of the certificate a TLS server presents
import tls from "node:tls";
// The expiry of the certificate a TLS server presents. Verification stays on:
// pass { ca } for a private CA, { rejectUnauthorized: false } only to inspect
// a certificate that no longer verifies.
export function serverNotAfter(host, port = 443, options = {}) {
return new Promise((resolve, reject) => {
const socket = tls.connect({ host, port, servername: host, ...options }, () => {
const cert = socket.getPeerCertificate();
socket.end();
resolve({ notAfter: new Date(cert.valid_to), authorized: socket.authorized, error: socket.authorizationError });
});
socket.setTimeout(10_000, () => socket.destroy(new Error(`timeout connecting to ${host}`)));
socket.on("error", reject);
});
}

How it works

  1. new X509Certificate(buffer) accepts PEM and DER. validToDate (Node.js 22.10+) is a Date; validTo is the same instant as text.
  2. tls.connect({ host, port, servername }) verifies the chain against Node's bundled roots and checks the host name; servername sends SNI so the server picks the right certificate.
  3. For a private CA, pass ca: it replaces the bundled roots for that connection. The run trusts only the test root and reads notAfter 2035-12-31T23:59:59.000Z from the server.
  4. getPeerCertificate() returns the leaf as an object; valid_to is a string that new Date() parses.

Gotchas

  • A verifying connection to an expired certificate never reaches the callback: the socket emits an error with code CERT_HAS_EXPIRED (certificate has expired). Catch it and report it as an expiry, not as a network failure.
  • To read the date of a certificate that no longer verifies, connect with rejectUnauthorized: false and check socket.authorized (false) and socket.authorizationError (CERT_HAS_EXPIRED) yourself. Never send data over such a connection.
  • validTo and valid_to are text (Dec 31 23:59:59 2035 GMT); compare Date objects or timestamps, not strings.
  • Math.floor on the difference gives negative days once a certificate has expired (-568 in the run), so a daysLeft < 30 check also catches certificates that are already past notAfter.

Related recipes