import * as pkijs from "pkijs"; // PKI.js: RFC 5280 path validation on top of crypto.subtle function parsePem(pem) { const base64 = pem.replace(/-----[^-]+-----|\s+/g, ""); const der = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); return pkijs.Certificate.fromBER(der);} // leafPem: the certificate to check; intermediatePems: what came with it; rootPems: what you trustexport async function verifyChain(leafPem, intermediatePems, rootPems, checkDate = new Date()) { const engine = new pkijs.CertificateChainValidationEngine({ trustedCerts: rootPems.map(parsePem), // PKI.js validates the LAST certificate of certs: the leaf goes at the end certs: [...intermediatePems.map(parsePem), parsePem(leafPem)], checkDate, }); const result = await engine.verify(); return { valid: result.result, code: result.resultCode, message: result.resultMessage };} How it works
parsePemstrips the PEM armour, decodes the base64 withatoband parses the DER withpkijs.Certificate.fromBER.trustedCertsholds your roots;certsthe intermediates followed by the leaf. Signature checks run throughcrypto.subtle.engine.verify()resolves to{ result, resultCode, resultMessage }:truefor the good chain, code 8 for the expired leaf, code -1 (No valid certificate paths found) when no path to a root exists.checkDatevalidates at another moment; it defaults to now.
Gotchas
- PKI.js validates the last certificate of
certs. Passing the expired leaf first makes it validate the intermediate and reporttrue— the run shows it. Keep the leaf last. - A look-alike root with the same name and a missing intermediate both end in code -1; the engine never downloads missing issuers from AIA URLs.
checkDateset to 2025-02-01 makes the expired leaf valid again: right for checking a signature made back then, wrong as a way around expiry.