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;} 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
new X509Certificate(buffer)accepts PEM and DER.validToDate(Node.js 22.10+) is aDate;validTois the same instant as text.tls.connect({ host, port, servername })verifies the chain against Node's bundled roots and checks the host name;servernamesends SNI so the server picks the right certificate.- For a private CA, pass
ca: it replaces the bundled roots for that connection. The run trusts only the test root and readsnotAfter 2035-12-31T23:59:59.000Zfrom the server. getPeerCertificate()returns the leaf as an object;valid_tois a string thatnew Date()parses.
Gotchas
- A verifying connection to an expired certificate never reaches the callback: the socket emits an error with
codeCERT_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: falseand checksocket.authorized(false) andsocket.authorizationError(CERT_HAS_EXPIRED) yourself. Never send data over such a connection. validToandvalid_toare text (Dec 31 23:59:59 2035 GMT); compareDateobjects or timestamps, not strings.Math.flooron the difference gives negative days once a certificate has expired (-568in the run), so adaysLeft < 30check also catches certificates that are already pastnotAfter.