import "reflect-metadata"; // @peculiar/x509 2.x needs a Reflect polyfill, imported firstimport * as x509 from "@peculiar/x509"; // built on WebCrypto, runs in the browser unchanged // Expiry of a certificate the user picked or pasted (PEM text, or DER bytes)export function expiry(input) { const cert = new x509.X509Certificate(input); const daysLeft = Math.floor((cert.notAfter.getTime() - Date.now()) / 86_400_000); return { subject: cert.subject, notAfter: cert.notAfter.toISOString(), daysLeft, expired: daysLeft < 0 };} // In a page: <input type="file" accept=".pem,.crt,.cer"> and// input.onchange = async () => console.log(expiry(await input.files[0].text())); How it works
new x509.X509Certificate(input)accepts PEM text, base64 or DER bytes (ArrayBuffer,Uint8Array);notAfteris aDate.reflect-metadatamust be imported before@peculiar/x5092.x, which relies on the Reflect metadata API.- The certificate never leaves the page: reading a file with
file.text()and parsing it are both local operations. - The run prints
2035-12-31T23:59:59.000Zfor the test leaf andexpired: truefor the certificate that ended on 31 March 2025.
Gotchas
Math.floormakes the day count negative after expiry (-568in the run), sodaysLeft < 30also flags certificates that have already expired.subjectis a string in RFC 4514 style with the attributes in reverse order (C=US, O=Example Corp, CN=api.example.com); usecert.subjectNamefor structured access rather than parsing the string.- The same function works on DER bytes: the run passes the decoded base64 of the PEM and gets the same
notAfter.