Trust stores: load and pin root certificate anchors
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
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
- spec RFC 5280 (NSS certdata.txt object stream)
- defends
trust-metadata-misattribution (CWE-345) - defends
trust-store-parser-DoS (CWE-770)
pki.trust.parseCcadbCsv
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
- spec RFC 4180
- spec RFC 5280 (CCADB certificate-records CSV)
- defends
trust-metadata-misattribution (CWE-345) - defends
trust-store-parser-DoS (CWE-770)
pki.trust.anchor
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();