@blamejs/pki 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +2 -2
- package/lib/constants.js +11 -0
- package/lib/lint.js +57 -0
- package/lib/path-validate.js +294 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.3.7 — 2026-07-17
|
|
8
|
+
|
|
9
|
+
Certification path building arrives as pki.path.build, and pki.lint gains seven RFC 5280 extension-criticality and CA-scope lints.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.path.build(leaf, opts) discovers the ordered certification path from a leaf up to a trust anchor over an untrusted pool of candidate CA certificates, then validates it -- the discovering complement of pki.path.validate. Candidates are matched by RFC 5280 name chaining, prioritized by the RFC 4158 heuristics (subjectKeyIdentifier/authorityKeyIdentifier match, anchor-adjacent issuer, CA and keyCertSign, validity), and searched depth-first with backtracking; every accept flows through pki.path.validate, so a name or key-identifier match is only an ordering hint and building never weakens a path-validation check. The search is bounded (a chain-length cap, a total-work cap on candidate expansions, and an identity-tuple visited-set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically; the trust store accepts anchor tuples or self-signed root certificates, opts.validate:false returns the ordered path unvalidated, and the verdict is cross-checked against openssl verify. AIA caIssuers fetching is offline-only (supply fetched issuers in opts.candidates). RFC 4158 / RFC 5280.
|
|
14
|
+
- pki.lint.certificate flags seven RFC 5280 extension-criticality and CA-scope violations that parse but breach the certificate profile: basicConstraints (on a certificate-signing CA), nameConstraints, policyConstraints, and inhibitAnyPolicy must be marked critical (error); keyUsage should be critical (warn); nameConstraints must appear only in a CA certificate (error); and an end-entity certificate should carry a subjectKeyIdentifier (notice). The basicConstraints criticality rule applies only when the CA key validates certificate signatures (RFC 5280 4.2.1.9), so a CRL-signing-only CA carrying a non-critical basicConstraints is not falsely flagged.
|
|
15
|
+
|
|
7
16
|
## v0.3.6 — 2026-07-17
|
|
8
17
|
|
|
9
18
|
pki.cmp.build gains the CA/responder side -- certificate, revocation, key-recovery, general, error, poll, and confirmation responses complete the RFC 9810 message surface.
|
package/README.md
CHANGED
|
@@ -221,7 +221,7 @@ is callable today; nothing below is a stub.
|
|
|
221
221
|
| `pki.acme` | RFC 8555 / 8737 / 8738 / 9773 ACME message layer over `pki.jose` — resource-object validators (closed status enums, conditional-required fields, unknown fields ignored), the three §7.1.6 state machines, request builders (newAccount + EAB, newOrder + `replaces`, finalize with CSR identifier-set match and account-key-reuse rejection, challenge responses, deactivation, revokeCert in both key modes, the keyChange nested JWS, POST-as-GET), the http-01 / dns-01 / tls-alpn-01 challenge computations, the dns/ip identifier validators, and the ARI certID (serial sign-padding preserved). A message layer, not an HTTP client — transport-injectable, fail-closed — `validate`, `identify`, `assertTransition`, the builders, `keyAuthorization`, `http01`, `dns01`, `tlsAlpn01Extension`, `verifyTlsAlpn01`, `ariCertId` |
|
|
222
222
|
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
223
223
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
224
|
-
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
224
|
+
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. `build(leaf, opts)` is the discovering complement (RFC 4158): from a leaf, an untrusted pool of candidate CA certificates, and a trust store, it finds the ordered leaf→anchor path `validate` accepts — name chaining plus the RFC 4158 §3.5 sort hints (AKI/SKI match, anchor-adjacent issuer, CA + keyCertSign, validity), a depth-first search with backtracking so the first path `validate` accepts wins, and a bounded search (chain-length cap, candidate-expansion cap, identity-tuple visited-set) so a cross-certificate cycle or Bridge-CA fan-out terminates deterministically; every accept flows through `validate` and its verdict is cross-checked against `openssl verify`. Pure and re-entrant, fail-closed — `validate`, `build`, `crlChecker`, `ocspChecker` |
|
|
225
225
|
| `pki.x509` | X.509 certificate issuance (RFC 5280 §4) — `sign(spec, issuer, opts)` builds and signs a certificate: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object; an `issuer` that is a key alone (self-signed — issuer equals subject, signed with that key) or a name + public key + key, or an issuing certificate + key (CA-signed). The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI auto-derived by SHA-1 of the subject key), subject alternative names, and certificate policies from the spec — any other extension supplied as pre-encoded DER — derives the version from the field set, and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`; every arm is independently verified by OpenSSL. Parsing stays at `pki.schema.x509.parse` — `sign` |
|
|
226
226
|
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985) — `sign(spec, key, opts)` builds and signs a `CertificationRequest`: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER; may be empty), the public key being certified, an optional `extensionRequest` (requested v3 extensions — subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, or an array of pre-encoded Extension DER — that a CA copies into the issued certificate), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned (what `openssl req -verify` checks). The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. Parsing stays at `pki.schema.csr.parse` — `sign` |
|
|
227
227
|
| `pki.attrcert` | RFC 5755 attribute-certificate issuance — `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` public-key-certificate reference, a `fromCertificate` binding derived from a certificate, or an object digest), the validity window (GeneralizedTime), an optional serial (positive, ≤ 20 octets; randomly generated when omitted), the `attributes` (the privilege syntaxes — role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo — or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER, each with its RFC 5755 criticality). An attribute certificate is never self-signed — the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
|
|
@@ -239,7 +239,7 @@ is callable today; nothing below is a stub.
|
|
|
239
239
|
| `pki.sigstore` | Offline verifier for a Sigstore bundle — the exact artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots + Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key; the ephemeral Fulcio certificate chain, validated as of the Rekor log time; the RFC 9162 inclusion proof folded to a Rekor-signed tree root; the log entry bound to this exact signature; and the in-toto SLSA subject digest the caller confirms against the published artifact. Zero runtime deps — reuses the X.509 parser, RFC 5280 path validator, and Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `pae`, `parseBundle`, `verifyBundle` |
|
|
240
240
|
| `pki.inspect` | Human-readable certificate inspection — the pure-JS equivalent of `openssl x509 -text`. `certificate(pem \| der \| parsed)` renders a familiar OpenSSL-style report: version, serial, signature algorithm, issuer/subject distinguished names, validity, public-key details (curve or modulus size + the raw point/modulus), every decoded extension with its critical flag, and the signature. Built over the strict parser and the two-way OID registry, so it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed extension falls back to a hex dump rather than throwing — `certificate` |
|
|
241
241
|
| `pki.webauthn` | WebAuthn / passkey attestation verification — offline trust evaluation of a W3C WebAuthn (Level 3) attestation. `parseAttestationObject(bytes)` decodes the CBOR attestation object + authenticatorData + COSE credential key over the strict `pki.cbor` codec; `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for **packed / tpm / android-key / apple / fido-u2f / none** — the x5c leaf key, the apple nonce, the tpm `certInfo` Name/`extraData` over the `pubArea`, the android `KeyDescription`, the fido-u2f `verificationData` — binding the credential public key to each attestation (via the signed authenticatorData for packed/fido-u2f, or a cert/`pubArea`-key equality check for android-key/apple/tpm) and enforcing each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers **ESP256/384/512, Ed25519, and Ed448** — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally-encoded DER ECDSA signature. A verifier, not a ceremony client; fail-closed with typed `webauthn/*` errors. Chaining the returned trust path to a pinned root (and aaguid→root via FIDO MDS) is the caller's step through `pki.path.validate` — `parseAttestationObject`, `verify` |
|
|
242
|
-
| `pki.lint` | Certificate linting — the zlint / pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded, advisory findings — each with a stable id, a severity (`fatal` > `error` > `warn` > `notice`), a source, a spec-clause citation, and a message — against the RFC 5280 profile plus a representative CA/Browser Forum TLS BR subset (serial sign/size, validity ordering + the
|
|
242
|
+
| `pki.lint` | Certificate linting — the zlint / pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded, advisory findings — each with a stable id, a severity (`fatal` > `error` > `warn` > `notice`), a source, a spec-clause citation, and a message — against the RFC 5280 profile plus a representative CA/Browser Forum TLS BR subset (serial sign/size, validity ordering + the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality — basicConstraints/nameConstraints/policyConstraints/inhibitAnyPolicy must be critical and keyUsage should be, nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI/AKI presence including the end-entity subjectKeyIdentifier, SAN required + CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys). Unlike every other entry the DATA path never throws: hostile bytes return a `fatal` `lint/unparseable` finding (with the strict parser's code) so a whole directory lints without a try/catch; only config-time misuse throws a typed `LintError`. `certificate`, `rules`, `profiles` |
|
|
243
243
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
244
244
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError` / `WebauthnError` / `LintError`, each carrying a stable `code` in `domain/reason` form |
|
|
245
245
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
|
package/lib/constants.js
CHANGED
|
@@ -204,6 +204,17 @@ var LIMITS = {
|
|
|
204
204
|
// chain's tree holds a handful of nodes; the operator may override via
|
|
205
205
|
// opts.maxPolicyNodes.
|
|
206
206
|
PATH_MAX_POLICY_NODES: 4096,
|
|
207
|
+
// Certification-path BUILDING bounds (pki.path.build). The pool of candidate
|
|
208
|
+
// issuers is untrusted, so a cross-certificate cycle or Bridge-CA fan-out can
|
|
209
|
+
// grow the number and length of candidate paths without bound (RFC 4158 sec.
|
|
210
|
+
// 8.1). PATH_BUILD_MAX_DEPTH caps the chain length explored from the leaf --
|
|
211
|
+
// well under PATH_MAX_CERTS so a built path clears the validator's own bound
|
|
212
|
+
// on hand-off; a real hierarchy is a handful of hops. PATH_BUILD_MAX_CANDIDATES
|
|
213
|
+
// is the total-work ceiling: the builder ticks it once per candidate-issuer
|
|
214
|
+
// expansion, so a hostile mesh terminates deterministically. Both are operator
|
|
215
|
+
// overridable (opts.maxDepth / opts.maxCandidatesConsidered).
|
|
216
|
+
PATH_BUILD_MAX_DEPTH: 20,
|
|
217
|
+
PATH_BUILD_MAX_CANDIDATES: 1000,
|
|
207
218
|
// PKCS#12 container ceilings. A PFX carries lists at three altitudes
|
|
208
219
|
// (ContentInfos per AuthenticatedSafe, SafeBags per SafeContents,
|
|
209
220
|
// attributes per bag) and can chain fresh DER blobs inside OCTET STRINGs,
|
package/lib/lint.js
CHANGED
|
@@ -297,6 +297,18 @@ function _ecCurveName(spki) {
|
|
|
297
297
|
} catch (_e) { return null; } // explicit / invalid EC parameters are not an approved named curve
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
|
|
301
|
+
// uniform: applies when the extension is present, fires when its raw `critical` flag is not
|
|
302
|
+
// true. The rule reads `ctx.raw(name).critical` WITHOUT decoding the value -- criticality is
|
|
303
|
+
// a structural property of the extension, independent of its contents.
|
|
304
|
+
function _criticalityRule(name, id, severity, citation, message) {
|
|
305
|
+
return {
|
|
306
|
+
id: id, severity: severity, source: "rfc5280", citation: citation, message: message,
|
|
307
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw(name); },
|
|
308
|
+
check: function (cert, ctx) { var e = ctx.raw(name); return (e && e.critical !== true) ? true : null; },
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
300
312
|
var RFC5280_RULES = [
|
|
301
313
|
{
|
|
302
314
|
id: "lint/rfc5280/serial-not-positive", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.2.2",
|
|
@@ -379,6 +391,51 @@ var RFC5280_RULES = [
|
|
|
379
391
|
appliesTo: function (cert) { return !!(cert.issuer && cert.subject && cert.issuer.dn !== cert.subject.dn); },
|
|
380
392
|
check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
|
|
381
393
|
},
|
|
394
|
+
{
|
|
395
|
+
// 4.2.1.9: MUST mark basicConstraints critical "in all CA certificates that contain
|
|
396
|
+
// public keys used to validate digital signatures on certificates" (keyCertSign). A CA
|
|
397
|
+
// key used exclusively for other purposes (CRL signing, key management) MAY carry a
|
|
398
|
+
// non-critical basicConstraints, so the rule applies only when keyCertSign is asserted
|
|
399
|
+
// (or keyUsage is absent) -- gating on cA alone would false-positive on a CRL-signing CA.
|
|
400
|
+
id: "lint/rfc5280/basic-constraints-not-critical", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.9",
|
|
401
|
+
message: "a CA certificate that validates certificate signatures must mark basicConstraints critical",
|
|
402
|
+
appliesTo: function (cert, ctx) {
|
|
403
|
+
var bc = ctx.decode("basicConstraints");
|
|
404
|
+
if (!(bc && bc.value && bc.value.cA === true)) return false;
|
|
405
|
+
var ku = ctx.decode("keyUsage");
|
|
406
|
+
return !ku || !ku.value || ku.value.keyCertSign === true;
|
|
407
|
+
},
|
|
408
|
+
check: function (cert, ctx) { var e = ctx.raw("basicConstraints"); return (e && e.critical !== true) ? true : null; },
|
|
409
|
+
},
|
|
410
|
+
_criticalityRule("nameConstraints", "lint/rfc5280/name-constraints-not-critical", "error",
|
|
411
|
+
"RFC 5280 4.2.1.10", "the nameConstraints extension must be marked critical"),
|
|
412
|
+
{
|
|
413
|
+
// 4.2.1.10: "The name constraints extension ... MUST be used only in a CA certificate."
|
|
414
|
+
id: "lint/rfc5280/name-constraints-not-ca", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.10",
|
|
415
|
+
message: "the nameConstraints extension must appear only in a CA certificate",
|
|
416
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("nameConstraints"); },
|
|
417
|
+
check: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return (bc && bc.value && bc.value.cA === true) ? null : true; },
|
|
418
|
+
},
|
|
419
|
+
_criticalityRule("policyConstraints", "lint/rfc5280/policy-constraints-not-critical", "error",
|
|
420
|
+
"RFC 5280 4.2.1.11", "the policyConstraints extension must be marked critical"),
|
|
421
|
+
_criticalityRule("inhibitAnyPolicy", "lint/rfc5280/inhibit-any-policy-not-critical", "error",
|
|
422
|
+
"RFC 5280 4.2.1.14", "the inhibitAnyPolicy extension must be marked critical"),
|
|
423
|
+
// 4.2.1.3: "When present, conforming CAs SHOULD mark this extension as critical." The
|
|
424
|
+
// recommendation directs the ISSUING CA whenever it includes keyUsage -- in ANY certificate,
|
|
425
|
+
// including an end-entity leaf -- and does NOT restrict it to certificates whose subject is
|
|
426
|
+
// itself a CA. So the rule is present-gated (matching zlint's w_ext_key_usage_not_critical),
|
|
427
|
+
// not CA-subject-gated; a SHOULD, hence warn.
|
|
428
|
+
_criticalityRule("keyUsage", "lint/rfc5280/key-usage-not-critical", "warn",
|
|
429
|
+
"RFC 5280 4.2.1.3", "the keyUsage extension should be marked critical"),
|
|
430
|
+
{
|
|
431
|
+
// 4.2.1.2: the SKI "SHOULD be included in all end entity certificates" (a SHOULD -> notice).
|
|
432
|
+
// The CA case is the separate ski-missing rule; this rule gates on the non-CA (leaf) path so
|
|
433
|
+
// the two are mutually exclusive and every certificate is covered by exactly one.
|
|
434
|
+
id: "lint/rfc5280/ski-missing-ee", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
|
|
435
|
+
message: "an end-entity certificate should carry a subjectKeyIdentifier extension",
|
|
436
|
+
appliesTo: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return !(bc && bc.value && bc.value.cA === true); },
|
|
437
|
+
check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
|
|
438
|
+
},
|
|
382
439
|
];
|
|
383
440
|
|
|
384
441
|
function _isTls(cert, ctx) { return ctx.isTlsServerCert; }
|
package/lib/path-validate.js
CHANGED
|
@@ -75,6 +75,8 @@ var OID = {
|
|
|
75
75
|
extKeyUsage: oid.byName("extKeyUsage"),
|
|
76
76
|
anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
|
|
77
77
|
cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
|
|
78
|
+
subjectKeyIdentifier: oid.byName("subjectKeyIdentifier"),
|
|
79
|
+
authorityKeyIdentifier: oid.byName("authorityKeyIdentifier"),
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
// The set of extension OIDs the validator PROCESSES -- an unrecognized critical
|
|
@@ -2024,8 +2026,300 @@ function verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2024
2026
|
});
|
|
2025
2027
|
}
|
|
2026
2028
|
|
|
2029
|
+
// ---- certification path BUILDING (pki.path.build, RFC 4158) ----------------
|
|
2030
|
+
|
|
2031
|
+
// A soft extension decode for the RFC 4158 sec. 3.5 SORT hints (AKI/SKI/
|
|
2032
|
+
// basicConstraints/keyUsage): a present-but-undecodable extension on an
|
|
2033
|
+
// UNTRUSTED pool candidate degrades to "no hint", never a hard fail -- the sort
|
|
2034
|
+
// weight is advisory, the branch is still tried, and validate is the authority.
|
|
2035
|
+
function softDecode(cert, extOid) {
|
|
2036
|
+
try { return decodeExt(cert, extOid); }
|
|
2037
|
+
catch (_e) { return null; }
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// dnEqual that fails a control-byte DN (CVE-2009-2408) closed to "not a match"
|
|
2041
|
+
// rather than rejecting the whole build -- one malformed pool cert must not
|
|
2042
|
+
// poison a buildable path (parity with selfIssued's swallow). The hard
|
|
2043
|
+
// name-chaining gate remains validate's own dnEqual over the chosen path.
|
|
2044
|
+
function nameMatchSoft(rdnsA, rdnsB) {
|
|
2045
|
+
try { return dnEqual(rdnsA, rdnsB); }
|
|
2046
|
+
catch (_e) { return false; }
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// A parsed extension entry the search's findExt dereferences by .oid and, for
|
|
2050
|
+
// the subjectAltName, by .value (a Buffer in the identity key).
|
|
2051
|
+
function _isExtensionEntry(e) { return !!e && typeof e.oid === "string" && Buffer.isBuffer(e.value); }
|
|
2052
|
+
|
|
2053
|
+
// The complete parsed-certificate shape build produces AND hands to validate --
|
|
2054
|
+
// every top-level field this module dereferences (grep-verified), each with the
|
|
2055
|
+
// type the code assumes. A claimed-parsed object satisfying this cannot throw a
|
|
2056
|
+
// raw TypeError anywhere in the search or the validate hand-off.
|
|
2057
|
+
function _isParsedCert(o) {
|
|
2058
|
+
return Buffer.isBuffer(o.tbsBytes) &&
|
|
2059
|
+
typeof o.serialNumberHex === "string" &&
|
|
2060
|
+
!!o.signatureAlgorithm && typeof o.signatureAlgorithm.oid === "string" &&
|
|
2061
|
+
!!o.signatureValue && Buffer.isBuffer(o.signatureValue.bytes) &&
|
|
2062
|
+
!!o.validity && o.validity.notBefore instanceof Date && o.validity.notAfter instanceof Date &&
|
|
2063
|
+
!!o.issuer && Array.isArray(o.issuer.rdns) &&
|
|
2064
|
+
!!o.subject && Array.isArray(o.subject.rdns) && Buffer.isBuffer(o.subject.bytes) &&
|
|
2065
|
+
!!o.subjectPublicKeyInfo && Buffer.isBuffer(o.subjectPublicKeyInfo.bytes) &&
|
|
2066
|
+
!!o.subjectPublicKeyInfo.algorithm && typeof o.subjectPublicKeyInfo.algorithm.oid === "string" &&
|
|
2067
|
+
!!o.subjectPublicKeyInfo.publicKey && Buffer.isBuffer(o.subjectPublicKeyInfo.publicKey.bytes) &&
|
|
2068
|
+
typeof o.subjectPublicKeyInfo.publicKey.unusedBits === "number" &&
|
|
2069
|
+
Array.isArray(o.extensions) && o.extensions.every(_isExtensionEntry);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function coerceCert(input) {
|
|
2073
|
+
// An already-parsed certificate is passed through; a DER Buffer / PEM string is
|
|
2074
|
+
// parsed (and its typed error normalized by the caller). A claimed-parsed object
|
|
2075
|
+
// (a truthy tbsBytes) must carry the COMPLETE parsed-certificate shape build and
|
|
2076
|
+
// the validate hand-off dereference -- a bare { tbsBytes } object, or any partial
|
|
2077
|
+
// shape, fails closed here as a typed PathError rather than a raw TypeError deeper
|
|
2078
|
+
// in the walk or inside validate.
|
|
2079
|
+
if (input && typeof input === "object" && !Buffer.isBuffer(input) && input.tbsBytes !== undefined) {
|
|
2080
|
+
if (!_isParsedCert(input)) throw E("path/bad-input", "build: an input has tbsBytes but is not a well-formed parsed certificate");
|
|
2081
|
+
return input;
|
|
2082
|
+
}
|
|
2083
|
+
return x509.parse(input);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// A trust-store entry is either a ready anchor tuple { name, publicKey,
|
|
2087
|
+
// algorithm, parameters? } or a self-signed root certificate reduced to that
|
|
2088
|
+
// tuple. The algorithm is the SPKI KEY-algorithm OID (the sec. 6.1.4(f)
|
|
2089
|
+
// parameter-inheritance value), mirroring trust.js _mkAnchor -- NOT the
|
|
2090
|
+
// signature OID. The anchor is an input to validate, never one of the path certs.
|
|
2091
|
+
function toAnchor(entry) {
|
|
2092
|
+
if (entry && typeof entry === "object" && !Buffer.isBuffer(entry) && entry.name && entry.publicKey && entry.algorithm) {
|
|
2093
|
+
// A ready anchor tuple: validate the shape build + validate consume -- name.rdns
|
|
2094
|
+
// (name matching), publicKey bytes and the algorithm OID (the sec. 6.1.4 key
|
|
2095
|
+
// hand-off). trustAnchors is a caller option, so a malformed tuple is a config
|
|
2096
|
+
// error and fails closed at entry, not a downstream no-path / soft verdict.
|
|
2097
|
+
if (!Array.isArray(entry.name.rdns) || !Buffer.isBuffer(entry.publicKey) || typeof entry.algorithm !== "string") {
|
|
2098
|
+
throw E("path/bad-input", "build: a trustAnchor tuple must be { name: { rdns: [...] }, publicKey: Buffer, algorithm: OID string }");
|
|
2099
|
+
}
|
|
2100
|
+
return entry;
|
|
2101
|
+
}
|
|
2102
|
+
var cert;
|
|
2103
|
+
try { cert = coerceCert(entry); }
|
|
2104
|
+
catch (e) { throw E("path/bad-input", "build: a trustAnchor entry must be a { name, publicKey, algorithm } tuple or a certificate", e); }
|
|
2105
|
+
var spki = cert.subjectPublicKeyInfo;
|
|
2106
|
+
return { name: cert.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters, subjectDer: cert.subject.bytes };
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// RFC 4158 sec. 2.4.2 loop key: the (subject DN + subjectAltName + subject
|
|
2110
|
+
// public key) tuple, NOT the DN alone. DN-alone keying wrongly prunes a
|
|
2111
|
+
// legitimate distinct cross-cert (cross-certs share a DN) and misses a same-key
|
|
2112
|
+
// loop; the key differentiates a rollover (same DN, new key) from a true repeat.
|
|
2113
|
+
function identityKey(cert) {
|
|
2114
|
+
var san = findExt(cert, OID.subjectAltName);
|
|
2115
|
+
return cert.subject.bytes.toString("hex") + "|" + (san ? san.value.toString("hex") : "") + "|" +
|
|
2116
|
+
cert.subjectPublicKeyInfo.bytes.toString("hex");
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
function childAkiKeyId(cert) {
|
|
2120
|
+
var d = softDecode(cert, OID.authorityKeyIdentifier);
|
|
2121
|
+
return (d && d.value && d.value.keyIdentifier) ? d.value.keyIdentifier : null;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// RFC 4158 sec. 3.5 candidate prioritization -- a SORT weight, never a gate.
|
|
2125
|
+
// Higher = tried first (fewer validate calls to reach a valid path). Every term
|
|
2126
|
+
// is a hint: a KID match, an anchor-adjacent issuer, CA + keyCertSign presence,
|
|
2127
|
+
// and validity at the check time all raise priority, but a lower-scoring
|
|
2128
|
+
// candidate is STILL attempted, and validate makes every accept decision.
|
|
2129
|
+
function scoreCandidate(cand, childAki, anchors, time) {
|
|
2130
|
+
var score = 0;
|
|
2131
|
+
if (childAki) {
|
|
2132
|
+
var d = softDecode(cand, OID.subjectKeyIdentifier);
|
|
2133
|
+
if (d && Buffer.isBuffer(d.value) && d.value.equals(childAki)) score += 1000; // sec. 3.5.12 KID match: heaviest
|
|
2134
|
+
}
|
|
2135
|
+
for (var i = 0; i < anchors.length; i++) {
|
|
2136
|
+
if (nameMatchSoft(cand.issuer.rdns, anchors[i].name.rdns)) { score += 100; break; } // sec. 3.5.15/.16 anchor-adjacent
|
|
2137
|
+
}
|
|
2138
|
+
var bc = softDecode(cand, OID.basicConstraints);
|
|
2139
|
+
if (bc && bc.value && bc.value.cA === true) score += 10; // sec. 3.5.1 basicConstraints cA
|
|
2140
|
+
var ku = softDecode(cand, OID.keyUsage);
|
|
2141
|
+
if (ku && ku.value && ku.value.keyCertSign === true) score += 10; // sec. 3.5.3 keyUsage keyCertSign
|
|
2142
|
+
var v = cand.validity;
|
|
2143
|
+
// allow:nan-date-comparison-unguarded -- cand.validity dates are codec-parsed (asn1 readTime rejects a NaN instant) and time is guard.time.assertValid'd at entry; the validity term is a fail-safe sort hint regardless.
|
|
2144
|
+
if (v && v.notBefore instanceof Date && v.notAfter instanceof Date &&
|
|
2145
|
+
v.notBefore.getTime() <= time.getTime() && time.getTime() <= v.notAfter.getTime()) score += 5; // sec. 3.5.4 validity
|
|
2146
|
+
return score;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* @primitive pki.path.build
|
|
2151
|
+
* @signature pki.path.build(leaf, opts) -> Promise<{ valid, path, trustAnchor, result, candidatesConsidered }>
|
|
2152
|
+
* @since 0.3.7
|
|
2153
|
+
* @status experimental
|
|
2154
|
+
* @spec RFC 4158, RFC 5280
|
|
2155
|
+
* @related pki.path.validate, pki.schema.x509.parse, pki.trust.parseCertdata
|
|
2156
|
+
*
|
|
2157
|
+
* Discover the ordered certification path from a leaf certificate up to a trust anchor, over an
|
|
2158
|
+
* untrusted pool of candidate CA certificates, then validate it. `build` is the discovering
|
|
2159
|
+
* complement of `validate`: `validate` takes an already-ordered path and a trust anchor and runs
|
|
2160
|
+
* the 6.1 state machine; `build` takes a leaf, an unordered pool of candidate issuers, and a
|
|
2161
|
+
* trust store, and searches for the ordered leaf->anchor path `validate` accepts.
|
|
2162
|
+
*
|
|
2163
|
+
* Candidate issuers are matched by RFC 5280 7.1 name chaining, prioritized by the RFC 4158 3.5
|
|
2164
|
+
* heuristics (a subjectKeyIdentifier/authorityKeyIdentifier match, an anchor-adjacent issuer,
|
|
2165
|
+
* CA + keyCertSign, validity at the check time -- all hints, never filters), and searched
|
|
2166
|
+
* depth-first with backtracking: the first ordered path that `pki.path.validate` accepts wins. A
|
|
2167
|
+
* name or key-identifier match is only an ordering hint; every accept flows through `validate`,
|
|
2168
|
+
* so `build` never weakens or duplicates a 6.1 check. The search over the untrusted pool is
|
|
2169
|
+
* bounded -- a depth cap on chain length, a total-work cap on candidate expansions, and a
|
|
2170
|
+
* visited-set keyed on the (subject, subjectAltName, public key) tuple -- so a cross-certificate
|
|
2171
|
+
* cycle or Bridge-CA fan-out terminates deterministically rather than growing without bound.
|
|
2172
|
+
*
|
|
2173
|
+
* `leaf` is a DER `Buffer`, a PEM string, or an already-parsed `pki.schema.x509` object. Returns
|
|
2174
|
+
* `{ valid, path, trustAnchor, result, candidatesConsidered }`, where `path` is the ordered array
|
|
2175
|
+
* `validate` consumes (anchor-proximal first, leaf last, the anchor excluded). Fail-closed: bad
|
|
2176
|
+
* options throw `path/bad-input`; no chain to any anchor throws `path/no-path`; chains that
|
|
2177
|
+
* assemble but none validate return `{ valid:false }` with the best failing `validate` result;
|
|
2178
|
+
* the search bound throws `path/build-limit`. AIA caIssuers fetching is out of scope -- `build` is
|
|
2179
|
+
* offline (zero network); supply fetched issuers in `opts.candidates`.
|
|
2180
|
+
*
|
|
2181
|
+
* @opts candidates The untrusted candidate CA pool (array of DER/PEM/parsed certs; alias `intermediates`).
|
|
2182
|
+
* @opts trustAnchors The trust store (non-empty array of `{ name, publicKey, algorithm }` tuples or self-signed root certificates).
|
|
2183
|
+
* @opts time The check date (`Date`, required); forwarded to every internal `validate` call.
|
|
2184
|
+
* @opts maxDepth Chain-length depth cap (default `C.LIMITS.PATH_BUILD_MAX_DEPTH`).
|
|
2185
|
+
* @opts maxCandidatesConsidered Total-work cap on candidate expansions (default `C.LIMITS.PATH_BUILD_MAX_CANDIDATES`).
|
|
2186
|
+
* @opts validate `false` returns the ordered path without validating (pure-builder mode; default `true`).
|
|
2187
|
+
* @opts (validate options) Every `pki.path.validate` option (`requiredEku`, `revocationChecker`, `checkPurpose`, the initial policy inputs, ...) is forwarded unchanged.
|
|
2188
|
+
* @example
|
|
2189
|
+
* var result = await pki.path.build(pemString, {
|
|
2190
|
+
* candidates: [], // untrusted intermediates (the openssl -untrusted set)
|
|
2191
|
+
* trustAnchors: [pemString], // a self-signed root, or a { name, publicKey, algorithm } tuple
|
|
2192
|
+
* time: new Date(),
|
|
2193
|
+
* });
|
|
2194
|
+
* result.valid; // true when a path to a trust anchor was found and validated
|
|
2195
|
+
*/
|
|
2196
|
+
async function build(leaf, opts) {
|
|
2197
|
+
opts = opts || {};
|
|
2198
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("path/bad-input", "build: opts must be an object");
|
|
2199
|
+
var leafCert;
|
|
2200
|
+
try { leafCert = coerceCert(leaf); }
|
|
2201
|
+
catch (e) { throw E("path/bad-input", "build: the leaf certificate did not parse", e); }
|
|
2202
|
+
|
|
2203
|
+
var poolInput = opts.candidates !== undefined ? opts.candidates : opts.intermediates;
|
|
2204
|
+
if (poolInput === undefined) poolInput = [];
|
|
2205
|
+
if (!Array.isArray(poolInput)) throw E("path/bad-input", "build: opts.candidates must be an array of certificates");
|
|
2206
|
+
// A pool larger than the absolute ceiling is rejected at entry (bounds the
|
|
2207
|
+
// parse work on an untrusted bundle) -- distinct from the per-search tick
|
|
2208
|
+
// budget below, which an operator may set lower to cap the DFS.
|
|
2209
|
+
var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
|
|
2210
|
+
if (poolInput.length > poolCeiling) throw E("path/bad-input", "build: the candidate pool has " + poolInput.length + " certificates, exceeding the " + poolCeiling + " ceiling");
|
|
2211
|
+
var pool;
|
|
2212
|
+
try { pool = poolInput.map(coerceCert); }
|
|
2213
|
+
catch (e) { throw E("path/bad-input", "build: a candidate certificate did not parse", e); }
|
|
2214
|
+
|
|
2215
|
+
if (!Array.isArray(opts.trustAnchors) || opts.trustAnchors.length === 0) throw E("path/bad-input", "build: opts.trustAnchors must be a non-empty array of anchor tuples or root certificates");
|
|
2216
|
+
var anchors = opts.trustAnchors.map(toAnchor);
|
|
2217
|
+
|
|
2218
|
+
guard.time.assertValid(opts.time, E, "path/bad-input", "build: opts.time (the validity-window check date, forwarded to validate)");
|
|
2219
|
+
// The chain-length bound is tied to the EFFECTIVE maxPathCerts the interleaved
|
|
2220
|
+
// validate uses: a built path holds hop + 1 certificates (the leaf plus each
|
|
2221
|
+
// intermediate), so capping hops at maxPathCerts - 1 keeps every path validate
|
|
2222
|
+
// sees within its own per-path ceiling -- otherwise a maxDepth of maxPathCerts
|
|
2223
|
+
// would assemble a maxPathCerts + 1 path that validate rejects with
|
|
2224
|
+
// path/bad-input. The same cap value is computed the way validate computes it
|
|
2225
|
+
// (opts.maxPathCerts forwarded), and the explicit-stack search below carries no
|
|
2226
|
+
// native-recursion stack risk regardless of the depth.
|
|
2227
|
+
var effectiveMaxCerts = guard.limits.cap(opts.maxPathCerts, "build: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
2228
|
+
// depthCeiling is 0 when maxPathCerts is 1: the only path within the limit is a
|
|
2229
|
+
// single leaf directly under an anchor (a zero-hop search). maxDepth's minimum
|
|
2230
|
+
// is therefore 0, not 1 -- a zero-hop search is legitimate and a maxPathCerts of
|
|
2231
|
+
// 1 must not throw before the anchor is even checked.
|
|
2232
|
+
var depthCeiling = effectiveMaxCerts - 1;
|
|
2233
|
+
var maxDepth = guard.limits.cap(opts.maxDepth, "build: opts.maxDepth", Math.min(constants.LIMITS.PATH_BUILD_MAX_DEPTH, depthCeiling), { E: E, code: "path/bad-input", min: 0, max: depthCeiling });
|
|
2234
|
+
var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
|
|
2235
|
+
var doValidate = opts.validate !== false;
|
|
2236
|
+
|
|
2237
|
+
// The build-specific options are consumed here; every remaining validate
|
|
2238
|
+
// option is forwarded unchanged to the interleaved validate call. Object.keys
|
|
2239
|
+
// enumerates only own enumerable properties, so no prototype-pollution belt.
|
|
2240
|
+
var BUILD_ONLY_OPT = { candidates: 1, intermediates: 1, trustAnchors: 1, maxDepth: 1, maxCandidatesConsidered: 1, validate: 1 };
|
|
2241
|
+
var forwarded = {};
|
|
2242
|
+
Object.keys(opts).forEach(function (k) { if (!BUILD_ONLY_OPT[k]) forwarded[k] = opts[k]; });
|
|
2243
|
+
function validateOpts(anchor) {
|
|
2244
|
+
var vo = {};
|
|
2245
|
+
Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
|
|
2246
|
+
vo.trustAnchor = anchor;
|
|
2247
|
+
return vo;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// The DoS terminator: tick() once per candidate-issuer expansion so a hostile
|
|
2251
|
+
// mesh cannot fan the search without bound (RFC 4158 8.1). A breach throws
|
|
2252
|
+
// path/build-limit. candidatesConsidered mirrors the tick count for the caller.
|
|
2253
|
+
var counter = guard.limits.counter(maxConsidered, E, "path/build-limit", "build: candidate-issuer expansion");
|
|
2254
|
+
var considered = 0;
|
|
2255
|
+
var anyChainAssembled = false;
|
|
2256
|
+
var bestFail = null;
|
|
2257
|
+
var success = null;
|
|
2258
|
+
|
|
2259
|
+
// A bounded, EXPLICIT-stack forward DFS from the leaf toward an anchor -- no
|
|
2260
|
+
// native recursion, so an operator-raised maxDepth cannot overflow the stack
|
|
2261
|
+
// over an untrusted graph. Each frame is a candidate chain [top, ...toward the
|
|
2262
|
+
// leaf] (leaf last) plus the identity keys already on it; a frame is expanded
|
|
2263
|
+
// by prepending a name-chaining issuer. Highest-priority candidates are pushed
|
|
2264
|
+
// LAST so they are popped (explored) FIRST -- depth-first in priority order.
|
|
2265
|
+
// Caps are enforced before every expansion; a candidate whose (subject, SAN,
|
|
2266
|
+
// public key) tuple is already on the chain is a loop and is pruned.
|
|
2267
|
+
var stack = [{ chain: [leafCert], hop: 0, keys: new Set([identityKey(leafCert)]) }];
|
|
2268
|
+
while (stack.length && !success) {
|
|
2269
|
+
var frame = stack.pop();
|
|
2270
|
+
var current = frame.chain[0];
|
|
2271
|
+
|
|
2272
|
+
// Terminate: is `current` issued by a configured anchor? An anchor-adjacent
|
|
2273
|
+
// cert completes a candidate path; validate is the authority on acceptance.
|
|
2274
|
+
// A found path breaks out here and the enclosing while exits on !success.
|
|
2275
|
+
for (var ai = 0; ai < anchors.length; ai++) {
|
|
2276
|
+
if (!nameMatchSoft(current.issuer.rdns, anchors[ai].name.rdns)) continue;
|
|
2277
|
+
if (!doValidate) { success = { path: frame.chain.slice(), trustAnchor: anchors[ai] }; break; }
|
|
2278
|
+
anyChainAssembled = true;
|
|
2279
|
+
var res = await validate(frame.chain, validateOpts(anchors[ai]));
|
|
2280
|
+
if (res.valid) { success = { valid: true, path: frame.chain.slice(), trustAnchor: anchors[ai], result: res }; break; }
|
|
2281
|
+
if (!bestFail) bestFail = { path: frame.chain.slice(), trustAnchor: anchors[ai], result: res };
|
|
2282
|
+
}
|
|
2283
|
+
if (success || frame.hop >= maxDepth) continue; // depth cap: this branch is exhausted
|
|
2284
|
+
|
|
2285
|
+
var childAki = childAkiKeyId(current);
|
|
2286
|
+
var scored = [];
|
|
2287
|
+
for (var pi = 0; pi < pool.length; pi++) {
|
|
2288
|
+
if (nameMatchSoft(pool[pi].subject.rdns, current.issuer.rdns)) {
|
|
2289
|
+
scored.push({ cand: pool[pi], score: scoreCandidate(pool[pi], childAki, anchors, opts.time) });
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
// Ascending sort -> push lowest priority first so the highest is on top.
|
|
2293
|
+
scored.sort(function (a, bb) { return a.score - bb.score; });
|
|
2294
|
+
for (var ci = 0; ci < scored.length; ci++) {
|
|
2295
|
+
counter.tick(); // breadth / total-work cap -> throws path/build-limit
|
|
2296
|
+
considered += 1;
|
|
2297
|
+
var cand = scored[ci].cand;
|
|
2298
|
+
var candKey = identityKey(cand);
|
|
2299
|
+
if (frame.keys.has(candKey)) continue; // the (subject,SAN,key) tuple is already on this branch -> a loop, prune
|
|
2300
|
+
var childKeys = new Set(frame.keys);
|
|
2301
|
+
childKeys.add(candKey);
|
|
2302
|
+
stack.push({ chain: [cand].concat(frame.chain), hop: frame.hop + 1, keys: childKeys });
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
if (success) {
|
|
2307
|
+
if (doValidate) return { valid: true, path: success.path, trustAnchor: success.trustAnchor, result: success.result, candidatesConsidered: considered };
|
|
2308
|
+
return { path: success.path, trustAnchor: success.trustAnchor, candidatesConsidered: considered };
|
|
2309
|
+
}
|
|
2310
|
+
if (anyChainAssembled) {
|
|
2311
|
+
// Chains reached an anchor but none validated -> the soft verdict carrying
|
|
2312
|
+
// the best failing validate result (parity with validate; never a throw).
|
|
2313
|
+
return { valid: false, path: bestFail.path, trustAnchor: bestFail.trustAnchor, result: bestFail.result, candidatesConsidered: considered };
|
|
2314
|
+
}
|
|
2315
|
+
// No chain to any configured anchor could even be assembled -- a permanent
|
|
2316
|
+
// structural verdict (name/key chaining dead-ended before the trust store).
|
|
2317
|
+
throw E("path/no-path", "build: no certification path from the leaf to any configured trust anchor could be assembled");
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2027
2320
|
module.exports = {
|
|
2028
2321
|
validate: validate,
|
|
2322
|
+
build: build,
|
|
2029
2323
|
crlChecker: crlChecker,
|
|
2030
2324
|
ocspChecker: ocspChecker,
|
|
2031
2325
|
verifyOcspResponse: verifyOcspResponse,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:dc01f613-ee62-4b7d-b2d5-42e3bb2f5560",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-18T01:11:02.665Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.7",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.7",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.7",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.7",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|