Certification path validation (RFC 5280 6)

RFC 5280 6 certification-path validation as a pure, re-entrant algorithm over already-parsed certificates. pki.path.validate(path, opts) runs the 6.1 state machine -- signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, and the certificate-policy tree -- and returns a structured verdict with a per-check reason code for every step. Validity-window enforcement is always on, with the check date an explicit input; the trust anchor is an input, never one of the validated certificates, and no input object is mutated.

Revocation is a pluggable hook: pki.path.crlChecker(crls) ships a CRL consultation built on pki.schema.crl.parse; an OCSP checker satisfies the same interface. Signature verification derives its algorithm from the certificate and the issuer key -- never from a value the message controls -- and fails closed on an unknown critical extension, an undetermined revocation status, or any structural fault.

pki.path.validate

since 0.1.16 experimental
pki.path.validate(path, opts) -> Promise<result>

Validate an ordered certification path (anchor->target) against a trust anchor per RFC 5280 6.1. path is an array of pki.schema.x509.parse objects (or DER/PEM the function parses); opts carries time (the always-on window check), trustAnchor ({ name, publicKey, algorithm, parameters? }), the 6.1.1 user-initial inputs (initialExplicitPolicy, initialAnyPolicyInhibit, initialPolicyMappingInhibit, userInitialPolicySet, and initialPermittedSubtrees / initialExcludedSubtrees -- arrays of { tag, base } where tag is the GeneralName tag number and base that form's constraint value), an optional requiredEku (key purposes -- registered OID names or dotted OID strings -- the target's extendedKeyUsage must assert; an absent extension is unrestricted, RFC 5280 4.2.1.12), and an optional revocationChecker. The value-carrying options (time, maxPathCerts, maxPolicyNodes, the subtree seeds, userInitialPolicySet, requiredEku) are validated at the entry point -- a mis-shaped value throws path/bad-input rather than silently not applying. Returns { valid, path, results, workingPublicKey, workingPublicKeyAlgorithm, workingPublicKeyParameters, validPolicyTree } where results[i].checks carries a per-check reason code (path/*) for every step. Pure and re-entrant -- no input object is mutated. An empty path or a missing anchor throws a typed PathError.

Example

async function example() {
  var cert = pki.schema.x509.parse(der);
  var res = await pki.path.validate([cert], {
    time: new Date("2020-01-01T00:00:00Z"),
    trustAnchor: { name: cert.issuer, publicKey: cert.subjectPublicKeyInfo.bytes, algorithm: cert.signatureAlgorithm.oid },
  });
  res.valid;  // boolean; res.results[0].checks carries the per-check codes
}
example();

References

pki.path.crlChecker

since 0.1.16 experimental
pki.path.crlChecker(crls) -> RevocationChecker

Build a CRL-backed RevocationChecker for pki.path.validate's revocationChecker option from a set of CRLs (DER/PEM or already-parsed). For each certificate it locates a CRL issued by the certificate's issuer, verifies the CRL signature over its tbsBytes, honors the issuing distribution point scope and reason coverage, checks currency (thisUpdate/nextUpdate), and reports { status: "good"|"revoked"| "unknown" }. A partitioned/sharded CRL (a critical IDP naming a distribution point) establishes "good" when it corresponds to one of the certificate's own cRLDistributionPoints -- at least one identically-encoded name in common (RFC 5280 sec. 6.3.3) -- and neither side restricts reason codes; a non-corresponding or reason-restricted shard is consulted for revocation only. An out-of-scope, stale, unauthorized, or unverifiable CRL yields unknown, which the validator fails closed unless softFail is set.

Example

var checker = pki.path.crlChecker([]);   // no CRLs -> every cert is "unknown"
typeof checker.check;                     // "function"

References

pki.path.ocspChecker

since 0.1.32 experimental
pki.path.ocspChecker(responses) -> RevocationChecker

Build an OCSP-backed RevocationChecker for pki.path.validate's revocationChecker option from a set of pre-fetched OCSP responses (DER/PEM or already-parsed). For each certificate it locates a SingleResponse whose CertID binds this cert's serial to its issuer -- recomputing issuerNameHash and issuerKeyHash under the CertID's own hashAlgorithm (SHA-1 or SHA-2), so a response using either matches -- confirms the responder is authorized (the issuing CA directly, or a valid CA-issued delegate bearing both id-kp-OCSPSigning and id-pkix-ocsp-nocheck), verifies the response signature over tbsResponseDataBytes, checks currency (thisUpdate/nextUpdate), and reports { status: "good"|"revoked"| "unknown" }. A wrong-issuer CertID, an unauthorized responder, a stale, not-yet-valid, nextUpdate-less, non-successful, or unverifiable response yields unknown, which the validator fails closed unless softFail is set; a revoked status surfaces its revocationReason. It is transport-free: the caller supplies bytes it collected (an OCSP fetch or a stapled response), so nonce anti-replay is the live client's responsibility and the residual replay defense is the thisUpdate/nextUpdate currency window.

Example

var checker = pki.path.ocspChecker([]);   // no responses -> every cert is "unknown"
typeof checker.check;                       // "function"

References

pki.path.verifyOcspResponse

since 0.2.22 experimental
pki.path.verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>

Verify a single already-parsed OCSP response for one certificate against its already-parsed issuer certificate at time -- the lower-level primitive pki.ocsp.verify composes after parsing its inputs (most callers want that ergonomic entry, which also handles DER/PEM decoding and request-nonce matching). It runs the EXACT SAME gates the path validator's ocspChecker does: it locates the SingleResponse whose CertID binds this cert's serial to its issuer (recomputing issuerNameHash/issuerKeyHash under the CertID's own hashAlgorithm), confirms the responder is authorized (the issuing CA directly, or a CA-issued delegate bearing both id-kp-OCSPSigning and id-pkix-ocsp-nocheck and passing the full out-of-path certificate gates), verifies the response signature over tbsResponseDataBytes, and checks currency (thisUpdate/nextUpdate) -- there is no weaker second OCSP verify path. It is fail-closed and never throws on an unauthorized, stale, or unverifiable response: those yield { status: "unknown" } with the granular responderAuthorized/signatureValid/matched flags and a reason; a revoked status surfaces its revocationReason. Setting opts.historicalMode treats a revocation whose revocationTime is strictly after time as not-yet- revoked (good) -- for validating a signature as of a past time, before the certificate was later revoked; the responder certificate is still validated at time either way. time must be a valid Date. A malformed response's parse fault surfaces as the parser's typed ocsp/* / asn1/* error.

Example

async function example() {
  var resp = pki.schema.ocsp.parseResponse(der);
  var v = await pki.path.verifyOcspResponse(resp, cert, issuerCert, new Date());
  v.status;   // "good" | "revoked" | "unknown"
}
example();

References

pki.path.build

since 0.3.7 experimental
pki.path.build(leaf, opts) -> Promise<{ valid, path, trustAnchor, result, candidatesConsidered, aiaFetches }>

Discover the ordered certification path from a leaf certificate up to a trust anchor, over an untrusted pool of candidate CA certificates, then validate it. build is the discovering complement of validate: validate takes an already-ordered path and a trust anchor and runs the 6.1 state machine; build takes a leaf, an unordered pool of candidate issuers, and a trust store, and searches for the ordered leaf->anchor path validate accepts.

Candidate issuers are matched by RFC 5280 7.1 name chaining, prioritized by the RFC 4158 3.5 heuristics (a subjectKeyIdentifier/authorityKeyIdentifier match, an anchor-adjacent issuer, CA + keyCertSign, validity at the check time -- all hints, never filters), and searched depth-first with backtracking: the first ordered path that pki.path.validate accepts wins. A name or key-identifier match is only an ordering hint; every accept flows through validate, so build never weakens or duplicates a 6.1 check. The search over the untrusted pool is bounded -- a depth cap on chain length, a total-work cap on candidate expansions, and a visited-set keyed on the (subject, subjectAltName, public key) tuple -- so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically rather than growing without bound.

leaf is a DER Buffer, a PEM string, or an already-parsed pki.schema.x509 object. Returns { valid, path, trustAnchor, result, candidatesConsidered }, where path is the ordered array validate consumes (anchor-proximal first, leaf last, the anchor excluded). Fail-closed: bad options throw path/bad-input; no chain to any anchor throws path/no-path; chains that assemble but none validate return { valid:false } with the best failing validate result; the search bound throws path/build-limit. By default build is OFFLINE (zero network) -- supply intermediates in opts.candidates. Set opts.fetchAia: true to opt in to fetching a MISSING intermediate from a certificate's Authority Information Access caIssuers URL (RFC 5280 sec. 4.2.2.1) over pki.transport: the fetch triggers only on a pool miss, every fetched certificate is UNTRUSTED pool material that still flows through validate when validation is on (never a trust anchor), and the whole surface is SSRF/amplification bounded -- https-only, a total fetch budget (a SILENT cap, never a throw that denies a buildable path), a per-cert URL cap, a build-wide URL dedupe, a response size + certificate-count cap, and no redirect following; every fetch fault is a silent skip. aiaFetches reports how many network GETs the build performed (0 when fetchAia is off). NOTE: with opts.validate:false (pure-builder mode) a fetched cert is returned unvalidated, identical to a static candidate -- the "flows through validate" guarantee needs validation on.

Options

(validate options)     Every `pki.path.validate` option (`requiredEku`, `revocationChecker`, `checkPurpose`, the initial policy inputs, ...) is forwarded unchanged.

Example

async function example() {
  var result = await pki.path.build(pemString, {
    candidates: [],              // untrusted intermediates (the openssl -untrusted set)
    trustAnchors: [pemString],   // a self-signed root, or a { name, publicKey, algorithm } tuple
    time: new Date(),
  });
  result.valid;                  // true when a path to a trust anchor was found and validated
}
example();

References