import { X509Certificate } from "node:crypto";import { readFileSync } from "node:fs";import * as pkijs from "pkijs"; // node:crypto has no path validator; PKI.js does RFC 5280 validation const load = (path) => pkijs.Certificate.fromBER(new X509Certificate(readFileSync(path)).raw); export async function verifyChain(leafPath, intermediatePaths, rootPaths, checkDate = new Date()) { const engine = new pkijs.CertificateChainValidationEngine({ trustedCerts: rootPaths.map(load), // PKI.js validates the LAST certificate of certs: the leaf goes at the end certs: [...intermediatePaths.map(load), load(leafPath)], checkDate, }); const { result, resultCode, resultMessage } = await engine.verify(); return { valid: result, code: resultCode, message: resultMessage };} import { X509Certificate } from "node:crypto";import { readFileSync } from "node:fs"; // One link only: did this issuer sign this certificate? (No dates, no CA flags, no path.)const leaf = new X509Certificate(readFileSync("expired.pem"));const issuer = new X509Certificate(readFileSync("intermediate.pem"));console.log("names match:", leaf.checkIssued(issuer), "signature valid:", leaf.verify(issuer.publicKey)); How it works
pkijs.Certificate.fromBERparses the DER thatX509Certificate.rawreturns, so files in PEM or DER work.trustedCertsare the roots;certsare the intermediates followed by the leaf. The engine builds the path, checks signatures, validity, basic constraints and name constraints.engine.verify()resolves to{ result, resultCode, resultMessage }—truefor the good chain, code 8 withThe certificate is either not yet valid or expiredfor the expired leaf.X509Certificate.checkIssued(issuer)andverify(issuer.publicKey)check one link: names and signature.
Gotchas
- PKI.js validates the last certificate of
certsas the end entity. With the leaf first, it validates the intermediate instead — and reports an expired leaf as valid. Put the leaf last, asverifyChaindoes. - The single-link check ignores dates and CA flags: the expired certificate passes
checkIssuedandverify(trueandtruein the run). Never use it as a trust decision. - A look-alike root and a missing intermediate both give
No valid certificate paths found(code -1); the engine does not fetch missing issuers.
Related recipes
- How to verify a certificate chain against your own trust store in WebCrypto
- How to verify a certificate chain against your own trust store with OpenSSL
- How to verify a certificate chain against your own trust store in Python
- How to check SSL certificate expiration date in Node.js
- How to decode an X.509 certificate in Node.js