Skip to content
ecdsa.com

Verify a certificate chain · WebCrypto

How to verify a certificate chain against your own trust store in WebCrypto

To verify a certificate chain in the browser, use PKI.js — a WebCrypto-based PKI library — and its CertificateChainValidationEngine: your roots go into trustedCerts, the intermediates and then the leaf into certs. Everything runs client-side on crypto.subtle. This recipe validates a leaf → intermediate → root chain from a test CA and the chains that must fail, and shows the argument-order trap.

Tested against the WebCrypto API in PKI.js 3.4.2 (WebCrypto in Node.js 24.22.0 (globalThis.crypto)) — the same crypto.subtle interface browsers implement.

verify.mjs — RFC 5280 path validation with PKI.js on crypto.subtle
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 trust
export 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

  1. parsePem strips the PEM armour, decodes the base64 with atob and parses the DER with pkijs.Certificate.fromBER.
  2. trustedCerts holds your roots; certs the intermediates followed by the leaf. Signature checks run through crypto.subtle.
  3. engine.verify() resolves to { result, resultCode, resultMessage }: true for the good chain, code 8 for the expired leaf, code -1 (No valid certificate paths found) when no path to a root exists.
  4. checkDate validates 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 report true — 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.
  • checkDate set to 2025-02-01 makes the expired leaf valid again: right for checking a signature made back then, wrong as a way around expiry.

Related recipes