X.509 chain (CA-bit issuer test)

The basicConstraints-enforcing issuer test the framework's own certificate-chain walkers route through (b.tsa.verifyToken, b.mail.bimi VMC/CMC, b.mail.crypto.smime, b.mdoc, b.contentCredentials, b.auth.fidoMds3). It exists because node:crypto's X509Certificate.checkIssued() validates the issuer/subject DN match, the AKI/SKI linkage, and — only when a keyUsage extension is present — keyCertSign, but it does not enforce basicConstraints cA:TRUE. A leaf / end-entity certificate (cA:FALSE) that omits keyUsage is therefore wrongly accepted as a signing CA for the next certificate in the chain — the classic basicConstraints bypass (CVE-2002-0862 class). Every in-tree walker routes its issuer test through these helpers so the cA enforcement can never be forgotten in one walker but present in another.

Exposed so a consumer validating an X.509 chain outside a TLS handshake — an operator-uploaded CA bundle, a non-handshake PQ-signed certificate — has the same hardened, fail-closed test instead of being pushed toward the raw checkIssued() path this module exists to prevent. Both helpers fail closed: any malformed input or unsupported key type returns false rather than throwing.

b.x509Chain.isCaCert(cert) #

stable0.15.15

True only when cert asserts basicConstraints cA:TRUE. node's X509Certificate exposes .ca (a boolean); a certificate with no basicConstraints extension or with cA:FALSE returns false. A missing cert or a non-boolean .ca (parse failure / unsupported runtime) fails closed to false.

var crypto = require("crypto");
var ca = new crypto.X509Certificate(caPem);
b.x509Chain.isCaCert(ca);   // → true only if basicConstraints cA:TRUE

b.x509Chain.issuerValidlyIssued(issuer, subject) #

stable0.15.15

True when issuer validly issued subject AND is itself a CA: the DN / AKI-SKI / keyUsage linkage (checkIssued), the cryptographic signature (verify), and basicConstraints cA:TRUE (isCaCert). The cA check runs first so a non-CA certificate is rejected before the expensive signature verification. Any exception (malformed cert, unsupported key type) fails closed to false.

var crypto = require("crypto");
var issuer  = new crypto.X509Certificate(issuerPem);
var subject = new crypto.X509Certificate(leafPem);
b.x509Chain.issuerValidlyIssued(issuer, subject);   // → boolean

b.x509Chain.canonicalNameKey(nameNode) #

stable0.20.37

One comparable string for an X.501 Name node, the shape a certificate's issuer and subject fields carry, so two encodings of the same name compare equal. Distinguished-name equality is not DER equality: RFC 5280 section 7.1 compares PrintableString and UTF8String attributes after normalization, so the same issuer spelled in either type, in either case, or with repeated inner spaces is one name. Attribute order within a relative distinguished name does not change the key, and attributes whose type is case-sensitive keep their case.

A case-insensitive attribute is prepared per RFC 4518: the soft hyphen, the combining grapheme joiner, the variation selectors, the zero width space and the control and format code points Unicode 3.2 defined are removed; tab, the line and page separators and every space-separator character become one space; the value is case FOLDED, not merely lowercased, so Straße CA and STRASSE CA are one name and so are the final and medial forms of sigma; and the value is normalized, to form C for the reason given below.

The removed set is a fixed table, not a category test against the Unicode version the runtime happens to ship, and any other code point in category Other refuses the whole name with null. Removing a character is the one mapping that can make two DIFFERENT names equal, so a character the RFC never listed must not be removed merely because a later Unicode gave it a format category: U+061C, added in Unicode 6.3, made an issuer A<U+061C>B take the key of AB. RFC 4518 section 2.4 prohibits the code points unassigned in Unicode 3.2, and refusing is the safe answer for one this cannot classify, since no conformant peer sends it.

That refusal reads the runtime's own category table, so which names are refused follows the Unicode version the Node release ships: a code point assigned a category after that release was built is refused there and may key on a newer one. The names this affects are the ones no conformant peer sends, and a name of letters, digits, spaces and punctuation keys identically on every release, but a deployment that compares verdicts across Node versions should pin one.

Normalization is to form C, not the form KC RFC 4518 names, and that is a deliberate narrowing. Compatibility normalization maps one character onto a DIFFERENT one, which under form KC gave 24,355 pairs of unrelated code points the same key: every superscript, subscript, circled, fullwidth and mathematical variant collapsed onto its ASCII base, so A<U+1D2C>B took the key of AAB and a SignerInfo could name a certificate it did not. The RFC bounds that hazard by prohibiting every code point unassigned in Unicode 3.2, which needs that repertoire to implement. Canonical normalization only composes and reorders, so it never maps a character onto another one: every set of code points that shares a key is a single Unicode case class, which is what a case-insensitive attribute is defined to unify. U+03D1 and U+03F4 share one because case folding takes both to theta. Two canonically equivalent spellings still key alike; two compatibility variants no longer do.

What that costs: a name spelled with a compatibility variant keys apart from the same name in ASCII. <U+FF21>CME CA, which is fullwidth A followed by CME CA, does not take the key of ACME CA, and the same holds for the Roman numeral <U+2160> against I and the <U+FB00> ligature against ff. A SignerInfo that names its issuer in one spelling and a certificate that names it in the other therefore do not match, and the verify reports the certificate as not named rather than accepting it. Both spellings are valid under RFC 4518, which asks for form KC so they unify. Form KC unifies them by mapping one character onto another, and bounding that to the characters the RFC allows needs the Unicode 3.2 repertoire; form C refuses the pair instead of risking the collapse. Vendoring that repertoire is what would let this use form KC, and until it does, a peer that mixes the two spellings of one name is refused rather than guessed at.

The steps run to a fixed point rather than in a chosen order, because no single order settles every name. Case mapping can hand back a decomposed sequence, which only a normalization that follows it recomposes. And the fold is the one step that cannot be undone: U+0345 becomes a base letter, so a mark run folded before it is canonically ordered puts the accent on a different letter for good, which removing a combining grapheme joiner can expose. So the mapping and the normalization settle together first, the fold runs once on the settled value, and that repeats until nothing changes, so the result keys to itself.

A PrintableString, IA5String or TeletexString is read only within its own repertoire: printable ASCII, and for PrintableString the letters, digits and '()+,-./:=? that type permits. Decoding bytes outside it as Latin-1 fed preparation characters that were never in the input, so the bytes 41 00 42 became A, NUL, B and the NUL was then removed, taking the key of the valid AB. Such a value now keys by its bytes.

Folding stops at the default rules: U+0131 is left alone, since Unicode folds it onto an ASCII i only under the Turkic rules, and merging the two would let one issuer read as another.

Case mapping is the runtime's, so a character Unicode assigned after 3.2 folds onto its case partner as that version defines: U+1C90 GEORGIAN MTAVRULI CAPITAL AN keys as U+10D0, the Mkhedruli letter it is the capital of. RFC 4518 would prohibit the newer character outright, which needs the Unicode 3.2 repertoire to implement, so this deviates from the RFC and matches the pair instead. It does not merge two DIFFERENT letters: every set of code points that shares a key is one Unicode case class, which is what a case-insensitive attribute is defined to do, and x509-chain-canonical-name.test.js asserts that over every code point.

An empty RDNSequence is a name and keys like any other, because RFC 5280 section 4.1.2.6 permits an empty subject when the identity is carried in subjectAltName. A structure that only resembles a name returns null instead: every node is checked for its tag, its class and whether it is constructed, so a relative distinguished name written as a SEQUENCE, one holding no attribute, an attribute that is not exactly a primitive object identifier and one value refuses rather than keying, each of which would otherwise key the same as the well-formed encoding and compare equal to it. A value whose tag number matches a string type under another class is not read as text: it keys by its bytes together with its full tag identity, which is also what a value this function cannot read as text does, so two such values are equal only when their bytes and their tag identity both are.

Two nulls are not a match: compare for equality only after checking that neither side is null, or two names you could not read would appear to equal each other.

var issuerKey  = b.x509Chain.canonicalNameKey(issuerNameNode);
var subjectKey = b.x509Chain.canonicalNameKey(subjectNameNode);
issuerKey !== null && issuerKey === subjectKey;
// → true when the two names are the same name, however each was encoded

b.x509Chain.sameName(aNameNode, bNameNode) #

stable0.20.37

Are these two X.501 Name nodes the same name? Answers from canonicalNameKey when both names can be prepared, so two encodings of one name match however each was written.

When the preparation refuses a name, the two are compared by their bytes instead of being called different. A refused name still equals itself, and the alternative refuses conforming input: a SignerInfo whose sid issuer field is copied verbatim from its own certificate stopped binding to it, and a self-issued rollover whose subject bytes equal its issuer bytes stopped counting as self-issued, so a chain RFC 5280 section 6.1.4 exempts from the path-length count was rejected. Byte equality is the strictest comparison there is, so it cannot make two different names match.

Returns false when either argument is missing, and false when one name prepares and the other does not.

b.x509Chain.sameName(subjectNameNode, issuerNameNode);
// → true when the two names are the same name, and when neither can be
//   prepared but their bytes are identical

b.x509Chain.pathLenSatisfied(chain) #

stable0.20.21

True when every CA in an ordered certificate chain honors its basicConstraints pathLenConstraint (RFC 5280 §4.2.1.9 / §6.1.4). chain is an array of node X509Certificate objects ordered leaf-first (index 0 is the end-entity, the last element is the topmost CA), the same order the framework's chain walkers build. A CA that asserts pathLenConstraint N permits at most N non-self-issued intermediate CAs between it and the end-entity; a chain that exceeds any such limit returns false. node's X509Certificate does not expose pathLenConstraint, so a chain built from otherwise-valid links can silently exceed it; this reads the constraint from each certificate's DER and enforces it. A chain shorter than two certificates has no CA link to constrain and returns true. A missing certificate in the array fails closed to false; a certificate with no pathLenConstraint imposes no limit.

var crypto = require("crypto");
var chain = [leafCert, intermediateCert, rootCert].map(function (pem) {
  return new crypto.X509Certificate(pem);
});
b.x509Chain.pathLenSatisfied(chain);   // → boolean

b.x509Chain.resolveChain(leaf, pool, anchors, opts?) #

stable0.20.21
{
  issued:    function,  // (issuer, subject) → boolean; default issuerValidlyIssued
  validAt:   function,  // (cert) → boolean; default () => true
  maxDepth:  number,    // default: pool.length + 1; caps the path length
  maxVisits: number,    // default: 4096; caps node expansions
}

Searches for a certification path from leaf through the candidate certificates in pool to any certificate in anchors, honoring basicConstraints pathLenConstraint. Every node on an accepted path passes opts.validAt, every adjacent pair passes opts.issued, and the assembled path satisfies pathLenSatisfied. When one candidate issuer overruns a path-length constraint the search tries the remaining candidates, so an interchangeable issuer (same subject and key, different pathLenConstraint) that completes a within-limit path is not preempted by a stricter one that appears earlier in the pool. A certificate already on the path under construction is never revisited, and the search stops after opts.maxVisits node expansions to bound work on a crafted pool.

The result carries reason "anchored" on success, "pathlen" when a matching anchor was rejected only for path length, "nameconstraint" when it was rejected only for a CA nameConstraints violation, otherwise "untrusted". invalidCert is the first certificate that failed validAt (null when none did). depthLimited is true when a branch was cut at maxDepth with an issuer still available.

var res = b.x509Chain.resolveChain(leaf, [subCa, issuer], [root]);
// → { ok: true, reason: "anchored", invalidCert: null, depthLimited: false }

b.x509Chain.nameConstraintsSatisfied(chain) #

stable0.20.22

Enforces X.509 nameConstraints (RFC 5280 §4.2.1.10 / §6.1.4) over an ordered chain [leaf, …intermediates, anchor]. Each CA's permittedSubtrees and excludedSubtrees accumulate down the chain (permitted intersect, excluded union) and are applied to the subjectAltName of every certificate below it (self-issued intermediates are exempt; the leaf is always checked). A dNSName is matched by the add-labels rule (base example.com is satisfied by example.com and host.example.com, not notexample.com); an iPAddress by the address/mask CIDR.

dNSName and iPAddress constraints are evaluated. A nameConstraints extension carrying any other subtree form (directoryName, rfc822Name, URI, otherName) or a non-default minimum/maximum, or one that does not parse, fails CLOSED (the chain is rejected) rather than being ignored. Returns a boolean; a non-array, or any entry that is not an X509Certificate, is false.

b.x509Chain.nameConstraintsSatisfied([leaf, intermediate, root]);
// → false when the leaf SAN is outside the intermediate's permitted subtree

Last updated 2026-10-12T14:21:56.879Z by seeder.