Trust stores: load and pin root certificate anchors

Mozilla / CCADB root-program ingestion into constraint-carrying trust anchors. The bare root list (tls.rootCertificates) throws away exactly the metadata that decides which roots may vouch for what: the per-purpose trust bits (a root trusted for TLS is not thereby trusted for S/MIME) and the per-purpose distrust-after dates (a sunsetting root keeps validating already-issued certificates while certificates issued after the cutoff are rejected). parseCertdata reads the NSS certdata.txt object stream and parseCcadbCsv the CCADB CSV export into one identical Anchor shape, so enforcement downstream is source-agnostic; anchor() hands an entry to pki.path.validate({ trustAnchor, checkPurpose }).

Everything is fail-closed and offline: the caller supplies the text (no network fetch); every malformed or oversized input throws a typed trust/* error before the offending allocation; a certificate object and its trust object are paired by byte-exact (CKA_ISSUER, CKA_SERIAL_NUMBER), never by adjacency, and cross-checked against the parsed DER, so trust metadata can never attach to the wrong root; only CKT_NSS_TRUSTED_DELEGATOR grants a purpose (everything else, including an absent bit, is untrusted).

pki.trust.parseCertdata

since 0.2.0 stable
pki.trust.parseCertdata(text) -> { anchors }

Parse the Mozilla/NSS certdata.txt root-store object stream into constraint-carrying trust anchors. Each CKO_CERTIFICATE object's MULTILINE_OCTAL CKA_VALUE is decoded and parsed as a DER certificate; the paired CKO_NSS_TRUST object (joined by byte-exact CKA_ISSUER + CKA_SERIAL_NUMBER, never adjacency, and cross-checked against the parsed DER) contributes the purpose trust bits (only CKT_NSS_TRUSTED_DELEGATOR grants a purpose); the per-purpose distrust-after dates ride in the certificate object as bare ASCII times routed through the strict DER time reader. Every anchor carries the exact { name, publicKey, algorithm, parameters } shape pki.path.validate consumes plus distrustAfter, purposes, subjectDer, label, mozillaCaPolicy. A certificate with no trust object becomes an anchor trusted for nothing (never silently dropped); a trust object with no certificate grants nothing. Malformed octal, an oversized block, an unrecognized trust value, a mispaired or ambiguous-duplicate block, or an undecodable distrust-after time throws a typed trust/* error, never a silently truncated or misattributed root.

Example

async function example() {
  // Real input is the NSS certdata.txt read from disk; a one-root stream is
  // synthesized here from a DER certificate to show the object shape.
  var pair = await pki.key.generate("Ed25519");
  var der = await pki.x509.sign({ subject: "Example Root", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
    extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"] } },
    { key: await pki.key.export(pair.privateKey) });
  var cert = pki.schema.x509.parse(der);
  var oct = function (buf) { return Array.prototype.map.call(buf, function (b) { return "\\" + ("000" + b.toString(8)).slice(-3); }).join(""); };
  var blk = function (n, v) { return n + " MULTILINE_OCTAL\n" + oct(v) + "\nEND\n"; };
  var store = pki.trust.parseCertdata("BEGINDATA\n\n" +
    "CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE\n" +
    blk("CKA_ISSUER", cert.issuer.bytes) +
    blk("CKA_SERIAL_NUMBER", pki.asn1.build.integer(cert.serialNumber)) +
    blk("CKA_VALUE", der));
  store.anchors[0].purposes.serverAuth;   // -> false (no trust object: trusted for nothing)
}
example();

References

pki.trust.parseCcadbCsv

since 0.2.0 stable
pki.trust.parseCcadbCsv(text) -> { anchors }

Parse a CCADB certificate-records CSV export into the same Anchor shape parseCertdata produces, so downstream enforcement is source-agnostic. Columns are located by header name, never by position, and unknown, reordered, or extra columns are tolerated; a MISSING required column (Common Name or Certificate Name, Trust Bits, Distrust for TLS After Date, Distrust for S/MIME After Date, PEM Info) fails closed with trust/bad-csv. Fields follow RFC 4180 quoting (embedded commas, newlines, doubled-quote escapes; the PEM Info column depends on it). Trust Bits is set-valued (Websites -> serverAuth, Email -> emailProtection, Code -> codeSigning; absent -> untrusted). A DATE-only distrust cell expands to the end-of-day ...T23:59:59Z instant, matching the NSS ...235959Z encoding, through the same strict time reader.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var pemText = await pki.x509.sign({ subject: "Example Root", subjectPublicKey: await pki.key.export(pair.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
    extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } },
    { key: await pki.key.export(pair.privateKey) }, { pem: true });
  var csv = "Common Name or Certificate Name,Trust Bits," +
            "Distrust for TLS After Date,Distrust for S/MIME After Date,PEM Info\n" +
            'Example Root,"Websites; Email",2027.06.01,,"' + pemText + '"';
  var store = pki.trust.parseCcadbCsv(csv);
  store.anchors[0].purposes.serverAuth;                    // -> true
  store.anchors[0].distrustAfter.serverAuth.toISOString(); // -> "2027-06-01T23:59:59.000Z"
}
example();

References

pki.trust.anchor

since 0.2.0 stable
pki.trust.anchor(entry, opts?) -> trustAnchor

Turn a parsed trust-store entry into the trustAnchor object pki.path.validate consumes: { name, publicKey, algorithm, parameters, distrustAfter, purposes }, a straight hand-off (validate reads distrustAfter as a per-purpose map and purposes as the delegator set, selected by its own opts.checkPurpose). With opts.purpose it fail-fasts: an entry that is not a trusted delegator for that purpose throws trust/purpose-not-trusted at build time, so an operator wiring a store catches the wrong root before a single validation runs (the authoritative gate stays inside validate).

opts.nameConstraints attaches the namespace a root program trusts a root for, when that is narrower than the root certificate itself states. Such a restriction lives in the program's data rather than in any nameConstraints extension the certificate carries, so it has to be supplied here. It is a { permitted, excluded } pair of { tag, base } subtrees, the same shape pki.path.validate takes for opts.initialPermittedSubtrees, and validate seeds it as the RFC 5280 sec. 6.1.1(h)(i) initial value: it intersects with every certificate's own constraints, so a leaf must satisfy both, and an excluded subtree rejects whatever is permitted. The subtrees are copied out of the caller's object, so changing that object afterwards does not change the namespace an anchor already vouches for. An overlay naming no subtree, or one supplying its subtrees through an accessor, is refused rather than carried, since either would widen the namespace the operator meant to restrict. A verdict reports anchorConstraints.nameConstraintsApplied.

A base is held to the form its tag names, and one outside that form is refused here rather than carried to a comparison that cannot reach a verdict on it. Tag 1 (rfc822Name) is a mailbox, a host name, or a host name written as a subtree with a leading dot, and a mailbox may name its domain as a bracketed address literal; tag 2 (dNSName) is a host name, with or without that leading dot; tag 6 (uniformResourceIdentifier) is the dotted host name a certificate's URI is compared by, not a URI; tag 4 (directoryName) is { rdns } naming at least one relative name, whose attribute types are dotted-decimal object identifiers and whose values are strings; tag 7 (iPAddress) is 8 bytes for IPv4 or 32 for IPv6, an address followed by its mask. A host name is labels of letters, digits and hyphens, with a hyphen at neither edge of a label, 63 characters to a label and 253 to the name, and it may carry the one trailing dot the comparison strips. Tags outside that set are refused.

Options

purpose: string   // "serverAuth" | "emailProtection" | "codeSigning"; fail-fast purpose check
nameConstraints: { permitted?: [{ tag, base }], excluded?: [{ tag, base }] }   // the root program's applied namespace; tags 1, 2, 4, 6, 7

Example

async function example() {
  var ca = await pki.key.generate("Ed25519");
  var caKey = await pki.key.export(ca.privateKey);
  var caDer = await pki.x509.sign({ subject: "Example Root", subjectPublicKey: await pki.key.export(ca.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
    extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"], subjectKeyIdentifier: true } },
    { key: caKey });
  var pemText = pki.schema.x509.pemEncode(caDer, "CERTIFICATE");
  var leaf = await pki.key.generate("Ed25519");
  var der = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(leaf.publicKey),
    notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
    extensions: { keyUsage: ["digitalSignature"], extendedKeyUsage: ["serverAuth"], authorityKeyIdentifier: true } },
    { cert: caDer, key: caKey });
  var csv = "Common Name or Certificate Name,Trust Bits," +
            "Distrust for TLS After Date,Distrust for S/MIME After Date,PEM Info\n" +
            'Example Root,Websites,,,"' + pemText + '"';
  var entry = pki.trust.parseCcadbCsv(csv).anchors[0];
  var anchor = pki.trust.anchor(entry, { purpose: "serverAuth" });
  await pki.path.validate([pki.schema.x509.parse(der)],
    { time: new Date("2026-06-01T00:00:00Z"), trustAnchors: anchor, checkPurpose: "serverAuth" });
}
example();

References

pki.trust.parseTrustAnchorList

since 0.8.13 stable
pki.trust.parseTrustAnchorList(input, opts?) -> { anchors }

Read an RFC 5914 TrustAnchorList into the trust-store entries pki.trust.anchor takes, the same shape parseCertdata and parseCcadbCsv produce. Each entry carries the anchor's name, public key and algorithm, so a list published by a root program drives pki.path.validate without the operator mapping it by hand.

A taInfo anchor's constraints ride on the entry and are applied as RFC 5937 section 3.2 states, which is that the stricter of the anchor's value and the caller's always wins: its name constraints intersect the caller's permitted subtrees and union its excluded ones, its policySet intersects opts.userInitialPolicySet, each of its policyFlags may turn the matching initial* option on and never off, and its pathLenConstraint bounds the path.

The certificate and tbsCert arms carry no constraints of their own, so an entry from either is the anchor that certificate's subject and key describe.

Options

- `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for the parse.

Example

async function example() {
  var pair = await pki.key.generate("Ed25519");
  var der = pki.trustanchor.build({ anchors: [{ taInfo: {
    pubKey: await pki.key.export(pair.publicKey), keyId: Buffer.alloc(20, 1),
    certPath: { taName: "CN=Example Root" } } }] });
  pki.trust.parseTrustAnchorList(der).anchors.length;   // -> 1
}
example();

References

pki.trust.discoversBundles

since 0.8.15 stable
pki.trust.discoversBundles(platform) -> boolean

Whether systemAnchors searches for a CA bundle on this platform. A Unix host's bundle IS the set its TLS stack trusts. macOS keeps its trust decisions in Keychain and Windows in a registry-backed store, and on macOS /etc/ssl/cert.pem can exist as OpenSSL's own copy, which is not the host's trust and omits both locally installed roots and Keychain overrides. Reporting that file as the host's anchors is the misattribution this verb exists to prevent, so those platforms are refused rather than searched. opts.path still reads an exported bundle anywhere.

Example

pki.trust.discoversBundles("linux");  // -> true
pki.trust.discoversBundles("darwin"); // -> false

References

  • spec RFC 5280
  • defends trust-store-misattribution (CWE-345)

pki.trust.systemAnchors

since 0.8.15 stable
pki.trust.systemAnchors(opts?) -> { anchors, source, path, skipped }

Read the host's CA bundle into the trust anchors pki.path.validate consumes. source names which set was read: "ca-bundle" for a file on this host, with path naming it, or "node-bundled" for the Mozilla set compiled into Node, whose path is null. A block the bundle carries that is not a certificate is listed in skipped with the label that named it, rather than dropped.

A host whose roots are not in a readable PEM file is refused, and the refusal names the command that exports them. macOS keeps its roots in a binary keychain and Windows in a registry-backed store, and reading either means running another program, which this toolkit does not do. Falling back to Node's set instead would report anchors the host does not use, so that takes the explicit opts.allowNodeBundle.

Options

- `path` -- read this file instead of searching the per-platform list.
- `allowNodeBundle` -- return Node's compiled-in set when no bundle can be read.
- `maxBytes` -- cap the bundle read (default `C.LIMITS.TRUST_MAX_BYTES`).

Example

// Read the host's bundle where there is one, and say so when there is not. Check `source`
// before trusting the answer: "node-bundled" is Node's compiled-in set, not this machine's.
var store = pki.trust.systemAnchors({ allowNodeBundle: true });
console.log(store.source);            // -> "ca-bundle" on a Unix host, else "node-bundled"
console.log(store.anchors.length > 0); // -> true
console.log(store.skipped.length >= 0); // -> true (entries named, never dropped silently)

References

  • spec RFC 7468 (PEM)
  • spec RFC 5280
  • defends trust-store-misattribution (CWE-345)
  • defends trust-store-parser-DoS (CWE-770)