@blamejs/pki 0.1.22 → 0.1.24

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.
@@ -9,9 +9,9 @@
9
9
  * @intro
10
10
  * RFC 5280 6 certification-path validation as a pure, re-entrant algorithm
11
11
  * over already-parsed certificates. `pki.path.validate(path, opts)` runs the
12
- * 6.1 state machine signature chaining, validity windows, name chaining,
12
+ * 6.1 state machine -- signature chaining, validity windows, name chaining,
13
13
  * basic constraints and path length, key usage, name constraints, and the
14
- * certificate-policy tree and returns a structured verdict with a per-check
14
+ * certificate-policy tree -- and returns a structured verdict with a per-check
15
15
  * reason code for every step. Validity-window enforcement is always on, with
16
16
  * the check date an explicit input; the trust anchor is an input, never one of
17
17
  * the validated certificates, and no input object is mutated.
@@ -19,12 +19,12 @@
19
19
  * Revocation is a pluggable hook: `pki.path.crlChecker(crls)` ships a CRL
20
20
  * consultation built on `pki.schema.crl.parse`; an OCSP checker satisfies the
21
21
  * same interface. Signature verification derives its algorithm from the
22
- * certificate and the issuer key never from a value the message controls
22
+ * certificate and the issuer key -- never from a value the message controls --
23
23
  * and fails closed on an unknown critical extension, an undetermined
24
24
  * revocation status, or any structural fault.
25
25
  *
26
26
  * @card
27
- * RFC 5280 6 certification-path validation run the 6.1 state machine over
27
+ * RFC 5280 6 certification-path validation -- run the 6.1 state machine over
28
28
  * an ordered path and a trust anchor for a structured, fail-closed verdict
29
29
  * with per-check reason codes. Pure and re-entrant.
30
30
  */
@@ -34,6 +34,7 @@ var pkix = require("./schema-pkix");
34
34
  var oid = require("./oid");
35
35
  var errors = require("./framework-error");
36
36
  var asn1 = require("./asn1-der");
37
+ var schema = require("./schema-engine");
37
38
  var x509 = require("./schema-x509");
38
39
  var crl = require("./schema-crl");
39
40
  var constants = require("./constants");
@@ -65,32 +66,40 @@ var OID = {
65
66
  subjectAltName: oid.byName("subjectAltName"),
66
67
  anyPolicy: oid.byName("anyPolicy"),
67
68
  emailAddress: oid.byName("emailAddress"),
69
+ extKeyUsage: oid.byName("extKeyUsage"),
70
+ anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
68
71
  };
69
72
 
70
- // The set of extension OIDs the validator PROCESSES an unrecognized critical
73
+ // The set of extension OIDs the validator PROCESSES -- an unrecognized critical
71
74
  // extension outside this set fails the path (6.1.4(o), 6.1.5(e)).
75
+ // extendedKeyUsage is recognized: the critical form is legal (4.2.1.12) and
76
+ // appears in the wild (RFC 6960 4.2.2.2 delegated OCSP responders); its
77
+ // structure is validated wherever it is critical, and key-purpose enforcement
78
+ // is the caller's opt-in via opts.requiredEku (RFC 5280 6.1 defines no EKU
79
+ // processing step -- the required purpose is application context).
72
80
  var PROCESSED_EXTENSIONS = {};
73
81
  [OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
74
- OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName].
82
+ OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
83
+ OID.extKeyUsage].
75
84
  forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
76
85
 
77
86
  // ---- signature verify bridge (NEW 6) ---------------------------------------
78
87
 
79
88
  // Signature-algorithm OID -> the WebCrypto verify descriptor + how to import
80
89
  // the issuer SPKI. Keyed via oid.byName so no dotted-decimal OID literal
81
- // appears in source (the registry owns arcname). The algorithm is a property
90
+ // appears in source (the registry owns arc<->name). The algorithm is a property
82
91
  // of the CERTIFICATE and the issuer key, never of a message-selected field
83
92
  // (CVE-2015-9235).
84
93
  var SIG_ALGS = {};
85
94
  // `params` is the REQUIRED AlgorithmIdentifier parameters shape: "null" (a DER
86
- // NULL must be present RSASSA-PKCS1-v1_5, RFC 4055 §5) or "absent" (parameters
87
- // must be omitted ECDSA/EdDSA/ML-DSA, RFC 5758/8410). A cert deviating from
95
+ // NULL must be present -- RSASSA-PKCS1-v1_5, RFC 4055 sec. 5) or "absent" (parameters
96
+ // must be omitted -- ECDSA/EdDSA/ML-DSA, RFC 5758/8410). A cert deviating from
88
97
  // its algorithm's required shape is malformed and rejected before verify.
89
98
  // `sameKeyOid` marks the one-shot families whose PUBLIC-KEY algorithm OID is the
90
- // SAME as the signature algorithm OID EdDSA, ML-DSA, SLH-DSA. For these, Node's
99
+ // SAME as the signature algorithm OID -- EdDSA, ML-DSA, SLH-DSA. For these, Node's
91
100
  // WebCrypto imports an SPKI of ANOTHER type under the requested name and verifies
92
101
  // with the real key (it does NOT reject a mismatched SPKI the way it does for
93
- // RSA/ECDSA), so the issuer-key <-> signature-algorithm consistency (RFC 9814 §4)
102
+ // RSA/ECDSA), so the issuer-key <-> signature-algorithm consistency (RFC 9814 sec. 4)
94
103
  // must be checked structurally: the SPKI OID must equal the signature OID.
95
104
  function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
96
105
  var entry = { verify: verify, imp: imp, params: params };
@@ -98,23 +107,23 @@ function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
98
107
  if (sameKeyOid) entry.sameKeyOid = true;
99
108
  SIG_ALGS[oid.byName(name)] = entry;
100
109
  }
101
- // RSASSA-PKCS1-v1_5 parameters MUST be NULL.
110
+ // RSASSA-PKCS1-v1_5 -- parameters MUST be NULL.
102
111
  _sig("sha256WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, "null");
103
112
  _sig("sha384WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, "null");
104
113
  _sig("sha512WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, "null");
105
- // ECDSA (hash in the OID; the curve comes from the imported key) params absent.
114
+ // ECDSA (hash in the OID; the curve comes from the imported key) -- params absent.
106
115
  _sig("ecdsaWithSHA256", { name: "ECDSA", hash: "SHA-256" }, { name: "ECDSA" }, "absent", true);
107
116
  _sig("ecdsaWithSHA384", { name: "ECDSA", hash: "SHA-384" }, { name: "ECDSA" }, "absent", true);
108
117
  _sig("ecdsaWithSHA512", { name: "ECDSA", hash: "SHA-512" }, { name: "ECDSA" }, "absent", true);
109
- // EdDSA (one-shot, no hash parameter) params absent; key OID == sig OID.
118
+ // EdDSA (one-shot, no hash parameter) -- params absent; key OID == sig OID.
110
119
  _sig("Ed25519", { name: "Ed25519" }, { name: "Ed25519" }, "absent", false, true);
111
120
  _sig("Ed448", { name: "Ed448" }, { name: "Ed448" }, "absent", false, true);
112
- // ML-DSA (FIPS 204) params absent; key OID == sig OID.
121
+ // ML-DSA (FIPS 204) -- params absent; key OID == sig OID.
113
122
  _sig("id-ml-dsa-44", { name: "ML-DSA-44" }, { name: "ML-DSA-44" }, "absent", false, true);
114
123
  _sig("id-ml-dsa-65", { name: "ML-DSA-65" }, { name: "ML-DSA-65" }, "absent", false, true);
115
124
  _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", false, true);
116
- // SLH-DSA (FIPS 205) params absent; key OID == sig OID. The twelve pure sets;
117
- // the RFC 9909 §3 OID name maps to the WebCrypto set name by id-slh-dsa-<set> ->
125
+ // SLH-DSA (FIPS 205) -- params absent; key OID == sig OID. The twelve pure sets;
126
+ // the RFC 9909 sec. 3 OID name maps to the WebCrypto set name by id-slh-dsa-<set> ->
118
127
  // SLH-DSA-<SET> (the webcrypto SLH_DSA_NODE keys). One-shot verify like ML-DSA.
119
128
  ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
120
129
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
@@ -126,7 +135,7 @@ _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", fal
126
135
  // RSASSA-PSS resolves its hash + salt from the AlgorithmIdentifier parameters.
127
136
  var OID_RSA_PSS = oid.byName("rsassaPss");
128
137
  var OID_MGF1 = oid.byName("mgf1");
129
- // SHA-1 is deliberately ABSENT a SHA-1 signature (PKCS#1 or PSS) is rejected,
138
+ // SHA-1 is deliberately ABSENT -- a SHA-1 signature (PKCS#1 or PSS) is rejected,
130
139
  // matching the no-sha1WithRSAEncryption posture (SHAttered chosen-prefix).
131
140
  var HASH_BY_OID = {};
132
141
  HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
@@ -134,7 +143,7 @@ HASH_BY_OID[oid.byName("sha384")] = "SHA-384";
134
143
  HASH_BY_OID[oid.byName("sha512")] = "SHA-512";
135
144
 
136
145
  var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
137
- // Curve group orders n a valid ECDSA signature has r,s in [1, n-1].
146
+ // Curve group orders n -- a valid ECDSA signature has r,s in [1, n-1].
138
147
  var CURVE_ORDER = {
139
148
  "P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
140
149
  "P-384": BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),
@@ -143,7 +152,7 @@ var CURVE_ORDER = {
143
152
 
144
153
  // The algorithm OID of an AlgorithmIdentifier SEQUENCE { algorithm OID,
145
154
  // parameters OPTIONAL }. STRICT: a universal SEQUENCE with the OID and AT MOST
146
- // one optional parameters element a bare [n]-wrapped OID (no SEQUENCE) or a
155
+ // one optional parameters element -- a bare [n]-wrapped OID (no SEQUENCE) or a
147
156
  // SEQUENCE carrying a spurious third element is malformed and must not be read
148
157
  // leniently as its named algorithm.
149
158
  function seqAlgOid(seq) {
@@ -153,14 +162,14 @@ function seqAlgOid(seq) {
153
162
  return asn1.read.oid(seq.children[0]);
154
163
  }
155
164
  // A hash AlgorithmIdentifier { OID, parameters? } whose parameters, when present,
156
- // MUST be DER NULL (RFC 4055 §2.1 / RFC 5754) never a SEQUENCE or arbitrary
165
+ // MUST be DER NULL (RFC 4055 sec. 2.1 / RFC 5754) -- never a SEQUENCE or arbitrary
157
166
  // value. Used for the PSS hashAlgorithm and the MGF1 inner hash.
158
167
  function hashAlgOid(seq) {
159
168
  var o = seqAlgOid(seq);
160
169
  if (seq.children.length === 2) {
161
170
  var p = seq.children[1];
162
171
  if (p.tagClass !== "universal" || p.tagNumber !== asn1.TAGS.NULL) throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier parameters must be NULL or absent (RFC 4055)");
163
- // A NULL is well-formed only with empty content (X.690 §8.8.2); the tag check
172
+ // A NULL is well-formed only with empty content (X.690 sec. 8.8.2); the tag check
164
173
  // alone would accept a non-empty NULL as valid parameters.
165
174
  try { asn1.read.nullValue(p); }
166
175
  catch (e) { throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier NULL parameters must have empty content (RFC 4055)", e); }
@@ -182,7 +191,7 @@ function resolveRsaPss(paramsBytes) {
182
191
  // under WebCrypto's defaults (a signatureAlgorithm bypass otherwise).
183
192
  // RFC 4055 DEFAULTs are SHA-1 (hashAlgorithm and mgf1SHA1). Because SHA-1 is
184
193
  // rejected, an absent hashAlgorithm or maskGenAlgorithm would resolve to SHA-1
185
- // and must be REJECTED a supported PSS AlgorithmIdentifier must state both
194
+ // and must be REJECTED -- a supported PSS AlgorithmIdentifier must state both
186
195
  // explicitly, with the MGF1 hash matching the signature hash.
187
196
  var hash = null, saltLength = 20, mgfNode = null, trailer = 1;
188
197
  if (!paramsBytes) throw E("path/unsupported-algorithm", "RSASSA-PSS requires explicit parameters (the SHA-1 defaults are rejected)");
@@ -199,7 +208,7 @@ function resolveRsaPss(paramsBytes) {
199
208
  pssLastTag = f.tagNumber;
200
209
  // Every RSASSA-PSS-params field is an EXPLICIT [n] wrapper (constructed)
201
210
  // around EXACTLY ONE value (an AlgorithmIdentifier or an INTEGER); a
202
- // primitive/childless or multi-child context field is malformed reading
211
+ // primitive/childless or multi-child context field is malformed -- reading
203
212
  // f.children[0] and ignoring the rest would accept non-DER parameters.
204
213
  if (!f.children || f.children.length !== 1) throw E("path/unsupported-algorithm", "malformed RSASSA-PSS parameter field [" + f.tagNumber + "] (an EXPLICIT wrapper carries exactly one value)");
205
214
  if (f.tagNumber === 0) {
@@ -212,7 +221,7 @@ function resolveRsaPss(paramsBytes) {
212
221
  var sl = asn1.read.integer(f.children[0]);
213
222
  // A negative saltLength must be rejected: the OpenSSL-backed verify shim
214
223
  // reads -1/-2/-3 as RSA_PSS_SALTLEN_DIGEST/AUTO/MAX, and AUTO (-2) accepts
215
- // a signature of ANY salt length defeating the salt-length binding.
224
+ // a signature of ANY salt length -- defeating the salt-length binding.
216
225
  // The upper bound keeps the value exact through Number conversion: the
217
226
  // verifier binds to the salt length the certificate states, so a value
218
227
  // that would round is not verifiable material (no real salt exceeds the
@@ -220,7 +229,7 @@ function resolveRsaPss(paramsBytes) {
220
229
  if (sl < 0n || sl > 2147483647n) throw E("path/unsupported-algorithm", "RSASSA-PSS saltLength must be a non-negative integer within the salt-length range");
221
230
  saltLength = Number(sl);
222
231
  } else if (f.tagNumber === 3) {
223
- // Compared for equality with 1 below bound before conversion so an
232
+ // Compared for equality with 1 below -- bound before conversion so an
224
233
  // oversized value cannot round on its way to the comparison.
225
234
  var tf = asn1.read.integer(f.children[0]);
226
235
  if (tf < 0n || tf > 2147483647n) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS trailerField " + tf.toString());
@@ -254,13 +263,13 @@ function resolveDescriptor(sigAlg) {
254
263
  return d;
255
264
  }
256
265
 
257
- // RFC 9814 §4 issuer-key <-> signature-algorithm consistency (algorithm-confusion
266
+ // RFC 9814 sec. 4 issuer-key <-> signature-algorithm consistency (algorithm-confusion
258
267
  // defense). For the one-shot families whose public key shares the signature OID
259
268
  // (EdDSA, ML-DSA, SLH-DSA), Node's WebCrypto imports an SPKI of a DIFFERENT type
260
- // under the requested name and verifies with the real key so an Ed25519-signed
269
+ // under the requested name and verifies with the real key -- so an Ed25519-signed
261
270
  // certificate labelled SLH-DSA would otherwise validate. Enforce structurally:
262
271
  // the issuer SPKI's algorithm OID MUST equal the signature algorithm OID. (For
263
- // RSA/ECDSA different key vs signature OIDs WebCrypto's import already rejects
272
+ // RSA/ECDSA -- different key vs signature OIDs -- WebCrypto's import already rejects
264
273
  // a mismatched key type, so `sameKeyOid` is not set and this is a no-op.)
265
274
  function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
266
275
  if (!d || !d.sameKeyOid) return;
@@ -268,7 +277,7 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
268
277
  try { keyOid = asn1.read.oid(asn1.decode(spkiBytes).children[0].children[0]); }
269
278
  catch (e) { throw E("path/algorithm-mismatch", "cannot read the issuer public-key algorithm identifier", e); }
270
279
  if (keyOid !== sigOid) {
271
- throw E("path/algorithm-mismatch", "issuer public-key algorithm " + keyOid + " does not match the signature algorithm " + sigOid + " (RFC 9814 §4 - algorithm confusion)");
280
+ throw E("path/algorithm-mismatch", "issuer public-key algorithm " + keyOid + " does not match the signature algorithm " + sigOid + " (RFC 9814 sec. 4 - algorithm confusion)");
272
281
  }
273
282
  }
274
283
 
@@ -321,7 +330,7 @@ function builtinVerify(state, cert) {
321
330
  return { ok: ok === true };
322
331
  }, function (e) {
323
332
  // A raw OpenSSL / WebCrypto fault (wrong key type for the declared
324
- // algorithm the algorithm-confusion case) is a signature failure, not a
333
+ // algorithm -- the algorithm-confusion case) is a signature failure, not a
325
334
  // path/* verdict of its own; only a PathError code is preserved.
326
335
  return { ok: false, code: pathCode(e, "path/bad-signature"), error: e };
327
336
  });
@@ -333,7 +342,7 @@ function normalizeAttrValue(v) {
333
342
  // RFC 5280 7.1 canonical DN comparison: case-fold + collapse internal
334
343
  // whitespace. Every standard X.520 DN attribute (CN/O/OU/L/ST/C/serialNumber/
335
344
  // dnQualifier) uses caseIgnoreMatch, and this canonical form matches OpenSSL's
336
- // X509_NAME_cmp applying it uniformly is the interoperable behavior, not a
345
+ // X509_NAME_cmp -- applying it uniformly is the interoperable behavior, not a
337
346
  // per-attribute caseExact rule that would reject a chain OpenSSL accepts.
338
347
  // Reject embedded NUL / control bytes so a truncation attack (CVE-2009-2408)
339
348
  // cannot make two different names compare equal.
@@ -390,7 +399,7 @@ function decodeExt(cert, extOid) {
390
399
  // RFC 5280 requires several CA-scoped extensions to be marked critical:
391
400
  // basicConstraints (4.2.1.9), nameConstraints (4.2.1.10),
392
401
  // policyConstraints (4.2.1.11), inhibitAnyPolicy (4.2.1.14). A conforming
393
- // validator rejects the non-critical form an extension a non-supporting
402
+ // validator rejects the non-critical form -- an extension a non-supporting
394
403
  // relying party would ignore must not silently pass here either. Returns a
395
404
  // typed PathError when a PRESENT extension is not critical, else null.
396
405
  function requireCriticalExt(ext, name, checks) {
@@ -406,7 +415,7 @@ function requireCriticalExt(ext, name, checks) {
406
415
  // Split an addr-spec into [localPart, host] at its single "@". Returns null when
407
416
  // there is no "@" (a bare host/domain constraint), or "ambiguous" when there are
408
417
  // multiple "@": a conformant certificate rfc822Name is a simple addr-spec with
409
- // EXACTLY ONE "@" (RFC 5280 §4.2.1.6 deprecates the quoted local part, and an
418
+ // EXACTLY ONE "@" (RFC 5280 sec. 4.2.1.6 deprecates the quoted local part, and an
410
419
  // addr-spec domain never contains "@"), so a multi-"@" mailbox like
411
420
  // "a@b"@example.com cannot be split reliably and must fail closed.
412
421
  function splitMailbox(addr) {
@@ -417,7 +426,7 @@ function splitMailbox(addr) {
417
426
  }
418
427
 
419
428
  function emailMatch(constraint, mailbox) {
420
- // RFC 5280 §4.2.1.10 rfc822Name: a constraint with an "@" is a full mailbox;
429
+ // RFC 5280 sec. 4.2.1.10 rfc822Name: a constraint with an "@" is a full mailbox;
421
430
  // a leading "." is a domain matching mailboxes at a SUBDOMAIN; otherwise it is
422
431
  // a host matching mailboxes AT that host only. RFC 5321: the local part is
423
432
  // CASE-SENSITIVE (exact); only the host is compared case-insensitively.
@@ -443,11 +452,11 @@ function emailMatch(constraint, mailbox) {
443
452
  }
444
453
 
445
454
  // Strip a single trailing dot (the absolute-FQDN root label) so "evil.com."
446
- // and "evil.com" compare equal otherwise a trailing-dot SAN would escape a
455
+ // and "evil.com" compare equal -- otherwise a trailing-dot SAN would escape a
447
456
  // dNSName constraint.
448
457
  function stripTrailingDot(s) { return s.charAt(s.length - 1) === "." ? s.slice(0, -1) : s; }
449
458
 
450
- // Host-suffix match with the RFC 5280 §4.2.1.10 leading-period rule shared by
459
+ // Host-suffix match with the RFC 5280 sec. 4.2.1.10 leading-period rule shared by
451
460
  // dNSName and uniformResourceIdentifier constraints on a host.
452
461
  function hostConstraintMatch(constraint, host) {
453
462
  var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
@@ -457,7 +466,7 @@ function hostConstraintMatch(constraint, host) {
457
466
  }
458
467
 
459
468
  // Is `host` a fully qualified domain name (as a URI host constraint requires,
460
- // RFC 5280 §4.2.1.10)? A dotless single label (localhost), an IPv4/IPv6 literal,
469
+ // RFC 5280 sec. 4.2.1.10)? A dotless single label (localhost), an IPv4/IPv6 literal,
461
470
  // or a value carrying non-hostname characters (a scheme "://", a path "/", a
462
471
  // port ":") is NOT a FQDN and cannot be matched against a domain-suffix
463
472
  // constraint. Only [A-Za-z0-9.-] with at least one dot qualifies.
@@ -470,7 +479,7 @@ function isFqdnHost(host) {
470
479
  }
471
480
 
472
481
  // A URI constraint applies to the host part: a leading "." matches subdomains
473
- // only; a bare host matches that host EXACTLY (not subdomains, per §4.2.1.10).
482
+ // only; a bare host matches that host EXACTLY (not subdomains, per sec. 4.2.1.10).
474
483
  // BOTH sides must be a fully qualified domain name: a URI SAN with no host / an
475
484
  // IP literal, OR a malformed constraint that is not an FQDN (e.g. a full URI
476
485
  // "http://blocked.example" rather than "blocked.example"), cannot be evaluated
@@ -493,7 +502,7 @@ function uriHost(uri) {
493
502
  var authority = m[1];
494
503
  // authority = [ userinfo "@" ] host [ ":" port ]. RFC 3986 userinfo does not
495
504
  // contain a raw "@", so a conformant authority has AT MOST ONE "@". Multiple
496
- // "@" is ambiguous the host cannot be determined reliably so fail CLOSED
505
+ // "@" is ambiguous -- the host cannot be determined reliably -- so fail CLOSED
497
506
  // (null -> uriMatch returns "unsupported" -> the caller rejects the path)
498
507
  // rather than guess a host that could slip a URI name constraint.
499
508
  var firstAt = authority.indexOf("@");
@@ -516,7 +525,7 @@ function ipMatch(constraint, addr) {
516
525
  // Does GeneralName-like {tagNumber, value} match a constraint base of the same
517
526
  // form? Returns true/false for a supported form, null when the forms differ
518
527
  // (not comparable), or the string "unsupported" when the forms are the SAME
519
- // but this validator does not implement that form's comparison the caller
528
+ // but this validator does not implement that form's comparison -- the caller
520
529
  // must then fail closed rather than treat it as "no match".
521
530
  function nameMatchesConstraint(gnTag, gnValue, base) {
522
531
  if (base.tagNumber !== gnTag) return null; // different form -> not comparable
@@ -549,7 +558,7 @@ function certNameForms(cert) {
549
558
  var hasRfc822San = false;
550
559
  if (san) {
551
560
  // Preserve EVERY SAN entry, including a form whose value the validator does
552
- // not decode (x400Address [3] / ediPartyName [5]) dropping it would let a
561
+ // not decode (x400Address [3] / ediPartyName [5]) -- dropping it would let a
553
562
  // critical constraint of that same unsupported form pass unenforced. The
554
563
  // constraint check fails such a form closed (name-constraint-unsupported).
555
564
  san.value.names.forEach(function (nm) {
@@ -557,10 +566,10 @@ function certNameForms(cert) {
557
566
  forms.push({ tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
558
567
  });
559
568
  }
560
- // RFC 5280 §4.2.1.10: the legacy emailAddress in the subject DN is checked as
569
+ // RFC 5280 sec. 4.2.1.10: the legacy emailAddress in the subject DN is checked as
561
570
  // an rfc822Name UNLESS the SAN already carries the email identity as an
562
571
  // rfc822Name entry. A SAN of a DIFFERENT form (e.g. dNSName only) does NOT
563
- // cover the email, so the subject-DN email must still be constrained else an
572
+ // cover the email, so the subject-DN email must still be constrained -- else an
564
573
  // excluded/non-permitted mailbox would slip an rfc822Name constraint.
565
574
  if (!hasRfc822San) {
566
575
  cert.subject.rdns.forEach(function (rdn) {
@@ -578,7 +587,7 @@ function certNameForms(cert) {
578
587
  // absorbing cert's permittedSubtrees (6.1.4(g)): the permitted set is tracked
579
588
  // as one GENERATION per absorbing cert, and a name of form F must match a
580
589
  // subtree of form F in EVERY generation that constrains form F. A flat pool
581
- // would compute the UNION letting a subordinate CA BROADEN what its parent
590
+ // would compute the UNION -- letting a subordinate CA BROADEN what its parent
582
591
  // permitted (a name-constraint bypass).
583
592
  function checkNameConstraints(state, cert) {
584
593
  var forms = certNameForms(cert);
@@ -670,16 +679,44 @@ function treeIsEmpty(tree) { return tree.children.length === 0; }
670
679
 
671
680
  // ---- the state machine -----------------------------------------------------
672
681
 
673
- function initialize(certs, params) {
682
+ // Is `base` the value shape the constraint matcher compares for a GeneralName
683
+ // form? Forms 0/3/5/8 (otherName / x400Address / ediPartyName / registeredID)
684
+ // carry any present value -- the matcher fails them closed as unsupported.
685
+ function isSubtreeBaseValid(tag, base) {
686
+ switch (tag) {
687
+ case 1: case 2: case 6: return typeof base === "string"; // rfc822Name / dNSName / URI
688
+ case 7: return (Buffer.isBuffer(base) || base instanceof Uint8Array) &&
689
+ (base.length === 8 || base.length === 32); // iPAddress: address + mask
690
+ case 4: return base !== null && typeof base === "object" && !!base.rdns && Array.isArray(base.rdns); // directoryName
691
+ default: return base !== undefined;
692
+ }
693
+ }
694
+
695
+ // Entry-point validation of a 6.1.1(b,c) user-initial subtree seed. A
696
+ // mis-shaped entry (e.g. the { base: { tagNumber, value } } shape the
697
+ // nameConstraints decoder emits) would never match any name, silently
698
+ // disabling the constraint the caller configured -- so it throws instead.
699
+ function checkedSubtreeSeeds(list, optName) {
700
+ if (list === undefined || list === null) return [];
701
+ if (!Array.isArray(list)) throw E("path/bad-input", "validate: opts." + optName + " must be an array of { tag, base } subtree entries");
702
+ return list.map(function (st) {
703
+ if (!st || typeof st !== "object" || !Number.isInteger(st.tag) || st.tag < 0 || st.tag > 8 || !isSubtreeBaseValid(st.tag, st.base)) {
704
+ throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
705
+ }
706
+ return { tag: st.tag, base: st.base };
707
+ });
708
+ }
709
+
710
+ function initialize(certs, params, seeds) {
674
711
  var n = certs.length;
675
712
  return {
676
713
  validPolicyTree: rootNode(),
677
714
  policyNodeCount: 1,
678
- maxPolicyNodes: params.maxPolicyNodes || 4096,
715
+ maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
679
716
  // Each absorbing cert's permittedSubtrees is one generation; a name must be
680
717
  // admitted by EVERY generation (intersection). An initial seed is generation 0.
681
- permittedGenerations: (params.initialPermittedSubtrees && params.initialPermittedSubtrees.length) ? [params.initialPermittedSubtrees.slice()] : [],
682
- excludedSubtrees: (params.initialExcludedSubtrees || []).slice(),
718
+ permittedGenerations: seeds.permitted.length ? [seeds.permitted] : [],
719
+ excludedSubtrees: seeds.excluded,
683
720
  explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
684
721
  inhibitAnyPolicy: params.initialAnyPolicyInhibit ? 0 : n + 1,
685
722
  policyMapping: params.initialPolicyMappingInhibit ? 0 : n + 1,
@@ -711,12 +748,15 @@ function processPolicies(state, cert, i, checks) {
711
748
  var policies = cp.value;
712
749
  var depth = i - 1;
713
750
  // anyPolicy processing is active only while inhibit_anyPolicy > 0, or for a
714
- // self-issued non-final cert (6.1.3(d)) this gates BOTH the (d)(1)(ii)
715
- // child-from-anyPolicy fallback and the (d)(2) expansion.
751
+ // self-issued non-final cert -- this gates ONLY the (d)(2) expansion of a
752
+ // cert-asserted anyPolicy. 4.2.1.14 inhibition is implemented entirely by
753
+ // that gate: a depth-(i-1) anyPolicy node created while processing was
754
+ // active remains matchable in (d)(1)(ii).
716
755
  var anyPolicyActive = state.inhibitAnyPolicy > 0 || (i < state._n && selfIssued(cert));
717
756
  var anyPolicyPresent = false;
757
+ var anyPolicyQualifiers = null;
718
758
  policies.forEach(function (p) {
719
- if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; return; }
759
+ if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
720
760
  var matched = false;
721
761
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
722
762
  if (node.expectedPolicySet.indexOf(p.policyIdentifier) !== -1) {
@@ -724,10 +764,11 @@ function processPolicies(state, cert, i, checks) {
724
764
  matched = true;
725
765
  }
726
766
  });
727
- if (!matched && anyPolicyActive) {
728
- // 6.1.3(d)(1)(ii): create the node from a depth-(i-1) anyPolicy node —
729
- // ONLY while anyPolicy processing is active (else a leaf policy would
730
- // wrongly survive after inhibit_anyPolicy reached 0).
767
+ if (!matched) {
768
+ // 6.1.3(d)(1)(ii): no expected-policy match -- create the node from a
769
+ // depth-(i-1) anyPolicy node. The RFC runs this step UNCONDITIONALLY
770
+ // (no inhibit clause); gating it would false-reject a path whose
771
+ // specific policy chains through a legitimately created anyPolicy node.
731
772
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
732
773
  if (node.validPolicy === OID.anyPolicy) addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
733
774
  });
@@ -735,10 +776,12 @@ function processPolicies(state, cert, i, checks) {
735
776
  });
736
777
  if (anyPolicyPresent && anyPolicyActive) {
737
778
  // 6.1.3(d)(2): expand anyPolicy into unmatched expected-policy values.
779
+ // Each generated child carries AP-Q -- the qualifier set of the
780
+ // certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
738
781
  leavesAt(state.validPolicyTree, depth).forEach(function (node) {
739
782
  node.expectedPolicySet.forEach(function (ep) {
740
783
  var already = node.children.some(function (ch) { return ch.validPolicy === ep; });
741
- if (!already) addChild(state, node, ep, null, [ep], checks);
784
+ if (!already) addChild(state, node, ep, anyPolicyQualifiers, [ep], checks);
742
785
  });
743
786
  });
744
787
  }
@@ -774,7 +817,7 @@ function isNullOrAbsentParams(p) {
774
817
  // Rebuild a SubjectPublicKeyInfo with the given AlgorithmIdentifier parameters
775
818
  // spliced in, so a key that inherited its domain parameters (an EC public key
776
819
  // whose SPKI omits the namedCurve, RFC 5280 6.1.4(f)/6.1.5(d)) becomes a
777
- // self-contained SPKI that importKey("spki", ) can consume.
820
+ // self-contained SPKI that importKey("spki", ...) can consume.
778
821
  function spliceSpkiParameters(spki, algOid, paramsBytes) {
779
822
  return asn1.build.sequence([
780
823
  asn1.build.sequence([asn1.build.oid(algOid), asn1.build.raw(paramsBytes)]),
@@ -804,9 +847,35 @@ function updateWorkingKey(state, cert) {
804
847
  state.workingPublicKeyAlgorithm = keyAlg.oid;
805
848
  }
806
849
 
850
+ // RFC 5280 4.2.1.12 -- when the caller states required key purposes, a cert
851
+ // carrying an extendedKeyUsage must assert every one (or anyExtendedKeyUsage).
852
+ // Applied to the TARGET cert (its own purposes) AND to every intermediate CA
853
+ // (EKU chaining: an EKU on a CA constrains the purposes below it), so marking
854
+ // extKeyUsage a PROCESSED critical extension is sound -- the semantic gate runs
855
+ // wherever the extension appears, never only on the leaf. A cert with no EKU is
856
+ // unconstrained. Returns true if the cert FAILS the required-purpose check.
857
+ function ekuPurposeFails(cert, requiredEku, checks) {
858
+ var eku;
859
+ try { eku = decodeExt(cert, OID.extKeyUsage); }
860
+ catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
861
+ if (!eku) return false; // absent EKU: unrestricted (4.2.1.12 restricts only when present)
862
+ var purposes = eku.value;
863
+ var ok = purposes.indexOf(OID.anyExtendedKeyUsage) !== -1 ||
864
+ requiredEku.every(function (p) { return purposes.indexOf(p) !== -1; });
865
+ checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
866
+ return !ok;
867
+ }
868
+
807
869
  function prepareNext(state, cert, i, checks) {
808
870
  var isSelfIssued = selfIssued(cert);
809
871
 
872
+ // RFC 5280 4.2.1.12 EKU chaining -- an intermediate CA's EKU constrains the
873
+ // purposes of the certs beneath it. Enforced here so a critical EKU on an
874
+ // intermediate (now a PROCESSED extension) is not merely tolerated but honored.
875
+ if (state.requiredEku && ekuPurposeFails(cert, state.requiredEku, checks)) {
876
+ return { fatal: true, error: E("path/eku-not-permitted", "an intermediate CA extendedKeyUsage does not permit a required purpose (RFC 5280 4.2.1.12)") };
877
+ }
878
+
810
879
  // (a),(b) policy mappings.
811
880
  var pm;
812
881
  try { pm = decodeExt(cert, OID.policyMappings); }
@@ -853,7 +922,7 @@ function prepareNext(state, cert, i, checks) {
853
922
  if (iapCritErr) return { fatal: true, error: iapCritErr };
854
923
  if (iap && iap.value < state.inhibitAnyPolicy) state.inhibitAnyPolicy = iap.value;
855
924
 
856
- // (k) basicConstraints cA gate the single authoritative CA check.
925
+ // (k) basicConstraints cA gate -- the single authoritative CA check.
857
926
  var bc;
858
927
  try { bc = decodeExt(cert, OID.basicConstraints); }
859
928
  catch (e) { checks.push({ name: "basicConstraints", ok: false, code: "path/bad-basic-constraints" }); return { fatal: true, error: e }; }
@@ -862,7 +931,7 @@ function prepareNext(state, cert, i, checks) {
862
931
  return { fatal: true, error: E("path/not-a-ca", "intermediate certificate is not a CA (basicConstraints cA is not TRUE, RFC 5280 6.1.4(k))") };
863
932
  }
864
933
  // 4.2.1.9: a CA certificate used to validate certificate signatures MUST mark
865
- // basicConstraints critical. A non-critical cA:TRUE is non-conforming a
934
+ // basicConstraints critical. A non-critical cA:TRUE is non-conforming -- a
866
935
  // relying party that skips non-critical extensions would not see the CA bit.
867
936
  var bcCritErr = requireCriticalExt(bc, "basicConstraints", checks);
868
937
  if (bcCritErr) return { fatal: true, error: bcCritErr };
@@ -889,7 +958,7 @@ function applyPolicyMappings(state, mappings, i) {
889
958
  if (state.policyMapping > 0) {
890
959
  // 6.1.4(b)(1): for each depth-i node whose valid_policy is an ID-P that
891
960
  // the extension maps, REPLACE its expected_policy_set with the SET of
892
- // subjectDomainPolicy values mapped from that ID-P (not append retaining
961
+ // subjectDomainPolicy values mapped from that ID-P (not append -- retaining
893
962
  // the pre-mapping policy would let a later cert satisfy the chain by
894
963
  // asserting the mapped-away policy).
895
964
  var mappedFrom = {}; // issuerDomainPolicy -> [subjectDomainPolicy, ...]
@@ -901,7 +970,7 @@ function applyPolicyMappings(state, mappings, i) {
901
970
  if (idpNodes.length) {
902
971
  idpNodes.forEach(function (nd) { nd.expectedPolicySet = mappedFrom[idp].slice(); });
903
972
  } else {
904
- // 6.1.4(b)(1): no depth-i ID-P node, but a depth-i anyPolicy node GENERATE
973
+ // 6.1.4(b)(1): no depth-i ID-P node, but a depth-i anyPolicy node -- GENERATE
905
974
  // the missing ID-P node under the anyPolicy node's parent with the mapped
906
975
  // expected set (else an anyPolicy-only CA loses the mapping).
907
976
  anyNodes.forEach(function (anyNode) {
@@ -912,7 +981,7 @@ function applyPolicyMappings(state, mappings, i) {
912
981
  } else {
913
982
  // 6.1.4(b)(2), policy_mapping == 0: delete every depth-i node whose
914
983
  // valid_policy is a mapped ID-P, then prune. A prior mapping in the same
915
- // extension may have already emptied the tree stop if it is gone.
984
+ // extension may have already emptied the tree -- stop if it is gone.
916
985
  var mappedSet = {};
917
986
  mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
918
987
  if (!state.validPolicyTree) return;
@@ -931,10 +1000,10 @@ function applyPolicyMappings(state, mappings, i) {
931
1000
  }
932
1001
 
933
1002
  // policyMappings is semantically processed ONLY in the prepare-for-next step
934
- // (§6.1.4(a),(b)), which does not run for the target certificate. It is also
935
- // SHOULD-be-non-critical (§4.2.1.5), so a CRITICAL policyMappings on the target
936
- // is both anomalous and unprocessed it must fail closed (§6.1.5(f)) rather
937
- // than let, e.g., a mapping to/from anyPolicy slip past the §6.1.4(a) rejection
1003
+ // (sec. 6.1.4(a),(b)), which does not run for the target certificate. It is also
1004
+ // SHOULD-be-non-critical (sec. 4.2.1.5), so a CRITICAL policyMappings on the target
1005
+ // is both anomalous and unprocessed -- it must fail closed (sec. 6.1.5(f)) rather
1006
+ // than let, e.g., a mapping to/from anyPolicy slip past the sec. 6.1.4(a) rejection
938
1007
  // the intermediate path applies. (nameConstraints / inhibitAnyPolicy are also
939
1008
  // prepare-next-only but are MUST-be-critical CA extensions, so a critical one on
940
1009
  // a terminal CA cert is conforming and is NOT treated as unprocessed here.)
@@ -952,7 +1021,7 @@ function unrecognizedCriticalExtension(cert, isTarget) {
952
1021
  }
953
1022
 
954
1023
  // Decode every RECOGNIZED critical extension to enforce that its extnValue is
955
- // structurally valid even where the semantic gate is skipped (the leaf).
1024
+ // structurally valid -- even where the semantic gate is skipped (the leaf).
956
1025
  // Returns the failing typed code, or null.
957
1026
  function validateCriticalExtensionStructure(cert) {
958
1027
  for (var i = 0; i < cert.extensions.length; i++) {
@@ -974,16 +1043,27 @@ function validateCriticalExtensionStructure(cert) {
974
1043
  * @spec RFC 5280
975
1044
  * @related pki.schema.x509.parse, pki.path.crlChecker
976
1045
  *
977
- * Validate an ordered certification `path` (anchortarget) against a trust
1046
+ * Validate an ordered certification `path` (anchor->target) against a trust
978
1047
  * anchor per RFC 5280 6.1. `path` is an array of `pki.schema.x509.parse`
979
1048
  * objects (or DER/PEM the function parses); `opts` carries `time` (the
980
1049
  * always-on window check), `trustAnchor` ({ name, publicKey, algorithm,
981
- * parameters? }), the optional policy inputs, and an optional
982
- * `revocationChecker`. Returns `{ valid, path, results, workingPublicKey,
983
- * workingPublicKeyAlgorithm, workingPublicKeyParameters, validPolicyTree }`
984
- * where `results[i].checks` carries a per-check reason code (`path/*`) for
985
- * every step. Pure and re-entrant no input object is mutated. An empty path
986
- * or a missing anchor throws a typed `PathError`.
1050
+ * parameters? }), the 6.1.1 user-initial inputs (`initialExplicitPolicy`,
1051
+ * `initialAnyPolicyInhibit`, `initialPolicyMappingInhibit`,
1052
+ * `userInitialPolicySet`, and `initialPermittedSubtrees` /
1053
+ * `initialExcludedSubtrees` -- arrays of `{ tag, base }` where `tag` is the
1054
+ * GeneralName tag number and `base` that form's constraint value), an
1055
+ * optional `requiredEku` (key purposes -- registered OID names or dotted OID
1056
+ * strings -- the target's extendedKeyUsage must assert; an absent extension
1057
+ * is unrestricted, RFC 5280 4.2.1.12), and an optional `revocationChecker`.
1058
+ * The value-carrying options (`time`, `maxPathCerts`, `maxPolicyNodes`, the
1059
+ * subtree seeds, `userInitialPolicySet`, `requiredEku`) are validated at the
1060
+ * entry point -- a mis-shaped value throws `path/bad-input` rather than
1061
+ * silently not applying. Returns `{ valid, path,
1062
+ * results, workingPublicKey, workingPublicKeyAlgorithm,
1063
+ * workingPublicKeyParameters, validPolicyTree }` where `results[i].checks`
1064
+ * carries a per-check reason code (`path/*`) for every step. Pure and
1065
+ * re-entrant -- no input object is mutated. An empty path or a missing anchor
1066
+ * throws a typed `PathError`.
987
1067
  *
988
1068
  * @example
989
1069
  * var cert = pki.schema.x509.parse(der);
@@ -1011,15 +1091,49 @@ async function validate(path, opts) {
1011
1091
  if (!(opts.time instanceof Date) || isNaN(opts.time.getTime())) {
1012
1092
  throw E("path/bad-input", "validate: opts.time must be a Date (the always-on validity-window check date)");
1013
1093
  }
1094
+ // Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
1095
+ // throws here rather than silently disabling the behavior it configures.
1096
+ if (opts.maxPolicyNodes !== undefined && (typeof opts.maxPolicyNodes !== "number" || !isFinite(opts.maxPolicyNodes) || opts.maxPolicyNodes < 1)) {
1097
+ throw E("path/bad-input", "validate: opts.maxPolicyNodes must be a positive number");
1098
+ }
1099
+ // user-initial-policy-set is a non-empty SET of policy OID strings (6.1.1(c)).
1100
+ // Membership is tested with indexOf, so a raw string here would be consulted
1101
+ // as a SUBSTRING match -- validated to an array of strings instead.
1102
+ if (opts.userInitialPolicySet !== undefined) {
1103
+ var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
1104
+ opts.userInitialPolicySet.every(function (p) { return typeof p === "string" && p.length > 0; });
1105
+ if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
1106
+ }
1107
+ var seeds = {
1108
+ permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
1109
+ excluded: checkedSubtreeSeeds(opts.initialExcludedSubtrees, "initialExcludedSubtrees"),
1110
+ };
1111
+ // opts.requiredEku -- the key purposes the TARGET certificate must be good
1112
+ // for, each a registered OID name or a dotted OID string. Resolved (and
1113
+ // typo-checked) here at the entry point.
1114
+ var requiredEku = null;
1115
+ if (opts.requiredEku !== undefined) {
1116
+ if (!Array.isArray(opts.requiredEku) || opts.requiredEku.length === 0) {
1117
+ throw E("path/bad-input", "validate: opts.requiredEku must be a non-empty array of key-purpose OID names or dotted OID strings");
1118
+ }
1119
+ requiredEku = opts.requiredEku.map(function (p) {
1120
+ if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
1121
+ if (/^\d+(\.\d+)+$/.test(p)) return p;
1122
+ var dotted = oid.byName(p);
1123
+ if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
1124
+ return dotted;
1125
+ });
1126
+ }
1014
1127
 
1015
- var state = initialize(certs, opts);
1128
+ var state = initialize(certs, opts, seeds);
1016
1129
  state._n = n;
1130
+ state.requiredEku = requiredEku;
1017
1131
  var verifier = opts.verifier || null;
1018
1132
  var revocationChecker = opts.revocationChecker || null;
1019
1133
  var softFail = opts.softFail === true;
1020
1134
  // Revocation is a pluggable, opt-in step: by default a path with no checker is
1021
1135
  // not revocation-checked. opts.requireRevocation makes the 6.1.3(a)(3)
1022
- // determination mandatory an absent checker (or an undetermined result)
1136
+ // determination mandatory -- an absent checker (or an undetermined result)
1023
1137
  // then fails the path closed instead of silently skipping the step.
1024
1138
  var requireRevocation = opts.requireRevocation === true;
1025
1139
  var failed = false;
@@ -1115,7 +1229,7 @@ async function validate(path, opts) {
1115
1229
  var lpc;
1116
1230
  try { lpc = decodeExt(cert, OID.policyConstraints); }
1117
1231
  catch (_e) { lpc = null; checks.push({ name: "policyConstraints", ok: false, code: "path/bad-policy" }); failed = true; }
1118
- // 4.2.1.11: policyConstraints MUST be critical apply the same check the
1232
+ // 4.2.1.11: policyConstraints MUST be critical -- apply the same check the
1119
1233
  // intermediate path (prepareNext) uses, so a non-critical policyConstraints
1120
1234
  // on the TARGET cert fails closed consistently.
1121
1235
  if (requireCriticalExt(lpc, "policyConstraints", checks)) failed = true;
@@ -1130,7 +1244,25 @@ async function validate(path, opts) {
1130
1244
  if (lpm && lpm.value.some(function (m) { return m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy; })) {
1131
1245
  checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
1132
1246
  }
1133
- updateWorkingKey(state, cert); // 6.1.5(c),(d) key AND algorithm AND parameters
1247
+ // 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
1248
+ // critical wherever they appear -- apply to the TARGET cert the same
1249
+ // check prepareNext applies to every intermediate (their semantic gates
1250
+ // do not run for the target, but the criticality rule still binds).
1251
+ var lnc;
1252
+ try { lnc = decodeExt(cert, OID.nameConstraints); }
1253
+ catch (e) { lnc = null; checks.push({ name: "nameConstraints", ok: false, code: pathCode(e, "path/bad-name-constraints") }); failed = true; }
1254
+ if (requireCriticalExt(lnc, "nameConstraints", checks)) failed = true;
1255
+ var liap;
1256
+ try { liap = decodeExt(cert, OID.inhibitAnyPolicy); }
1257
+ catch (e) { liap = null; checks.push({ name: "inhibitAnyPolicy", ok: false, code: pathCode(e, "path/bad-policy") }); failed = true; }
1258
+ if (requireCriticalExt(liap, "inhibitAnyPolicy", checks)) failed = true;
1259
+ // 4.2.1.12: when the caller states required key purposes, the target's
1260
+ // extendedKeyUsage must assert every one (or anyExtendedKeyUsage -- the
1261
+ // 4.2.1.12 wildcard; rejecting it is an application MAY, not the
1262
+ // default). An ABSENT extension leaves the key unrestricted, so it
1263
+ // satisfies any required purpose.
1264
+ if (requiredEku && ekuPurposeFails(cert, requiredEku, checks)) failed = true;
1265
+ updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
1134
1266
  }
1135
1267
 
1136
1268
  // 6.1.4(o) / 6.1.5(e) unrecognized critical extension.
@@ -1140,7 +1272,7 @@ async function validate(path, opts) {
1140
1272
  // A RECOGNIZED critical extension must still be structurally valid even
1141
1273
  // when its semantic gate does not run on this cert (the leaf is not subject
1142
1274
  // to 6.1.4, so its basicConstraints/keyUsage/policy* are never read in
1143
- // prepareNext) a malformed critical extnValue must fail closed, not slip
1275
+ // prepareNext) -- a malformed critical extnValue must fail closed, not slip
1144
1276
  // through as "recognized". Decode every recognized critical extension to
1145
1277
  // validate it (a no-op for one already decoded above; the decoders are pure).
1146
1278
  var crit = validateCriticalExtensionStructure(cert);
@@ -1205,39 +1337,51 @@ function userConstrainedPolicies(state, n) {
1205
1337
  var OID_IDP = oid.byName("issuingDistributionPoint");
1206
1338
  var OID_DELTA_CRL = oid.byName("deltaCRLIndicator");
1207
1339
 
1340
+ // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
1341
+ // onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
1342
+ // onlySomeReasons [3] ReasonFlags OPTIONAL, indirectCRL [4] DEFAULT FALSE,
1343
+ // onlyContainsAttributeCerts [5] DEFAULT FALSE } (RFC 5280 sec. 5.2.5). Declared
1344
+ // through the engine so the trailing-field grammar (strictly-ascending tags, each
1345
+ // at most once) and the DER BOOLEAN value rules are the shared enforcement, not a
1346
+ // hand-walk; a present DEFAULT-FALSE flag encoding FALSE is the omitted default
1347
+ // (X.690 sec. 11.5) and rejects at the leaf-value level below.
1348
+ var IDP_SCHEMA = schema.seq([
1349
+ schema.trailing([
1350
+ { tag: 0, name: "distributionPoint", schema: schema.any() },
1351
+ { tag: 1, name: "onlyContainsUserCerts", schema: schema.implicitBoolean(1) },
1352
+ { tag: 2, name: "onlyContainsCACerts", schema: schema.implicitBoolean(2) },
1353
+ { tag: 3, name: "onlySomeReasons", schema: schema.implicitBitString(3) },
1354
+ { tag: 4, name: "indirectCRL", schema: schema.implicitBoolean(4) },
1355
+ { tag: 5, name: "onlyContainsAttributeCerts", schema: schema.implicitBoolean(5) },
1356
+ ], { minTag: 0, maxTag: 5, unexpectedCode: "path/bad-idp", orderCode: "path/bad-idp" }),
1357
+ ], { assert: "sequence", code: "path/bad-idp", what: "IssuingDistributionPoint" });
1358
+
1208
1359
  function decodeIdp(ext) {
1209
- // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0]?,
1210
- // onlyContainsUserCerts [1]?, onlyContainsCACerts [2]?, onlySomeReasons [3]?,
1211
- // indirectCRL [4]?, onlyContainsAttributeCerts [5]? }. Surface the scope
1212
- // flags the checker gates on.
1360
+ // Surface the scope flags the checker gates on. ANY structural or value fault
1361
+ // -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
1362
+ // an encoded-FALSE default -- leaves the CRL's scope unknown: the CRL is
1363
+ // unusable, never assumed unrestricted.
1213
1364
  var out = { hasDistributionPoint: false, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
1214
- var n;
1215
- try { n = asn1.decode(ext.value); }
1365
+ var m;
1366
+ try { m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS); }
1216
1367
  catch (_e) { out.malformed = true; return out; }
1217
- // A present IDP whose value is not a SEQUENCE leaves the CRL's scope unknown
1218
- // treat the CRL as unusable rather than assuming an unrestricted scope.
1219
- if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children) { out.malformed = true; return out; }
1220
- n.children.forEach(function (f) {
1221
- if (f.tagClass !== "context") { out.malformed = true; return; }
1222
- if (f.tagNumber === 0) out.hasDistributionPoint = true; // scoped to a specific DP
1223
- else if (f.tagNumber === 1) { if (idpBoolTrue(f, out)) out.onlyUser = true; }
1224
- else if (f.tagNumber === 2) { if (idpBoolTrue(f, out)) out.onlyCa = true; }
1225
- else if (f.tagNumber === 3) out.onlySomeReasons = true; // BIT STRING; presence gates out-of-scope
1226
- else if (f.tagNumber === 4) { if (idpBoolTrue(f, out)) out.indirect = true; }
1227
- else if (f.tagNumber === 5) { if (idpBoolTrue(f, out)) out.onlyAttr = true; }
1228
- else out.malformed = true;
1229
- });
1368
+ function flag(f) {
1369
+ if (!f.present) return false;
1370
+ // A present flag must encode DER-TRUE; a FALSE is the omitted DEFAULT
1371
+ // (X.690 sec. 11.5), so mark the scope unknown and report not-set.
1372
+ var isSet = f.value === true;
1373
+ if (!isSet) out.malformed = true;
1374
+ return isSet;
1375
+ }
1376
+ out.hasDistributionPoint = m.fields.distributionPoint.present;
1377
+ out.onlyUser = flag(m.fields.onlyContainsUserCerts);
1378
+ out.onlyCa = flag(m.fields.onlyContainsCACerts);
1379
+ out.onlySomeReasons = m.fields.onlySomeReasons.present ? true : null;
1380
+ out.indirect = flag(m.fields.indirectCRL);
1381
+ out.onlyAttr = flag(m.fields.onlyContainsAttributeCerts);
1230
1382
  return out;
1231
1383
  }
1232
1384
 
1233
- // An IMPLICIT BOOLEAN scope flag is primitive with the single DER-TRUE octet
1234
- // 0xFF (a present DEFAULT-FALSE field is TRUE); anything else — constructed,
1235
- // wrong length, non-0xFF — is malformed, so mark the scope unknown.
1236
- function idpBoolTrue(f, out) {
1237
- if (f.children || !f.content || f.content.length !== 1 || f.content[0] !== 0xff) { out.malformed = true; return false; }
1238
- return true;
1239
- }
1240
-
1241
1385
  /**
1242
1386
  * @primitive pki.path.crlChecker
1243
1387
  * @signature pki.path.crlChecker(crls) -> RevocationChecker
@@ -1265,20 +1409,40 @@ function crlChecker(crls) {
1265
1409
  check: async function (cert, issuer, ctx) {
1266
1410
  var time = ctx.time;
1267
1411
  var historical = ctx.historicalMode === true;
1268
- var certIsCa = (function () { try { var bc = decodeExt(cert, OID.basicConstraints); return bc && bc.value.cA === true; } catch (_e) { return false; } })();
1269
- // RFC 5280 §6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
1270
- // certificate, the cRLSign bit must be set. An issuer that OMITS keyUsage is
1271
- // unconstrained (the same rule the §6.1.4(n) keyCertSign gate applies to
1272
- // certificate signing), so its CRL is authoritative. The anchor is likewise
1273
- // unconstrained (issuerCert is null for the cert it directly issued).
1412
+ // The cert's CA-ness gates the IDP scope flags (onlyContainsUserCerts /
1413
+ // onlyContainsCACerts, RFC 5280 sec. 6.3.3(b)(2)). An UNREADABLE
1414
+ // basicConstraints leaves that scope undeterminable -- guessing "not a
1415
+ // CA" would consult a user-only CRL for what may be a CA certificate
1416
+ // (out of the CRL's scope, so its silence proves nothing) -- so certIsCa
1417
+ // stays null, BOTH scoped forms are skipped below, and only a
1418
+ // full-scope CRL can speak for this certificate. The decode fault is
1419
+ // carried into the undetermined verdict's reason.
1420
+ var certIsCa = null, certScopeFault = null;
1421
+ try {
1422
+ var bc = decodeExt(cert, OID.basicConstraints);
1423
+ certIsCa = !!(bc && bc.value.cA === true);
1424
+ } catch (e) {
1425
+ certScopeFault = pathCode(e, "path/bad-extension-value");
1426
+ }
1427
+ // RFC 5280 sec. 6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
1428
+ // certificate, the cRLSign bit must be VERIFIED set. An issuer that OMITS
1429
+ // keyUsage is unconstrained (the same rule the sec. 6.1.4(n) keyCertSign gate
1430
+ // applies to certificate signing), so its CRL is authoritative. The anchor
1431
+ // is likewise unconstrained (issuerCert is null for the cert it directly
1432
+ // issued). A PRESENT-but-unreadable keyUsage cannot be verified -- treating
1433
+ // it like an absent one would let garbage keyUsage bytes authorize CRL
1434
+ // signing -- so no CRL from this issuer can be authoritative.
1274
1435
  var signerAuthorized = true;
1275
1436
  if (issuer && issuer.issuerCert) {
1276
1437
  var iku;
1277
- try { iku = decodeExt(issuer.issuerCert, OID.keyUsage); } catch (_e) { iku = null; }
1438
+ try { iku = decodeExt(issuer.issuerCert, OID.keyUsage); }
1439
+ catch (e) {
1440
+ return { status: "unknown", reason: "the CRL issuer's keyUsage extension is unreadable (" + pathCode(e, "path/bad-key-usage") + "), so its authorization to sign CRLs cannot be verified" };
1441
+ }
1278
1442
  if (iku && iku.value.cRLSign !== true) signerAuthorized = false;
1279
1443
  }
1280
1444
 
1281
- // Consult EVERY CRL issued by the cert's issuer a clean CRL must not
1445
+ // Consult EVERY CRL issued by the cert's issuer -- a clean CRL must not
1282
1446
  // shadow a revoking one (RFC 5280 6.3.3). A serial listed in ANY
1283
1447
  // authoritative, in-scope, current, verified CRL is revoked; the cert is
1284
1448
  // "good" only if at least one such CRL was consulted and none list it;
@@ -1291,7 +1455,7 @@ function crlChecker(crls) {
1291
1455
  var theCrl = parsed[k];
1292
1456
  // dnEqual throws on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
1293
1457
  // A single malformed CRL in the bundle must NOT abort the whole check (which
1294
- // would mask a later authoritative CRL and pass under softFail) treat it
1458
+ // would mask a later authoritative CRL and pass under softFail) -- treat it
1295
1459
  // as unusable and skip it, consulting the remaining CRLs.
1296
1460
  var issuerMatches;
1297
1461
  try { issuerMatches = dnEqual(theCrl.issuer.rdns, cert.issuer.rdns); }
@@ -1300,7 +1464,7 @@ function crlChecker(crls) {
1300
1464
  if (!signerAuthorized) continue;
1301
1465
 
1302
1466
  // A CRL carrying deltaCRLIndicator is a DELTA CRL: it lists only the
1303
- // CHANGES since a base CRL (RFC 5280 §5.2.4). deltaCRLIndicator is a
1467
+ // CHANGES since a base CRL (RFC 5280 sec. 5.2.4). deltaCRLIndicator is a
1304
1468
  // RECOGNIZED extension (so a critical one is not "unhandled"); the delta
1305
1469
  // is acted on only AFTER it passes the currency + signature checks below,
1306
1470
  // so a stale, malformed, or unverifiable delta cannot spuriously block a
@@ -1313,20 +1477,20 @@ function crlChecker(crls) {
1313
1477
 
1314
1478
  // A validly-signed CRL carrying a CRITICAL extension this checker does
1315
1479
  // not understand (anything but issuingDistributionPoint / deltaCRLIndicator)
1316
- // may change the CRL's scope or meaning treat it as unusable (RFC 5280
1480
+ // may change the CRL's scope or meaning -- treat it as unusable (RFC 5280
1317
1481
  // 5.2 critical-extension semantics), never authoritative.
1318
1482
  var unhandledCritical = false;
1319
1483
  for (var x = 0; x < theCrl.crlExtensions.length; x++) {
1320
1484
  var xe = theCrl.crlExtensions[x];
1321
1485
  if (xe.critical && xe.oid !== OID_IDP && xe.oid !== OID_DELTA_CRL) { unhandledCritical = true; break; }
1322
1486
  }
1323
- // RFC 5280 §5.3: a critical CRL-ENTRY extension the checker cannot
1487
+ // RFC 5280 sec. 5.3: a critical CRL-ENTRY extension the checker cannot
1324
1488
  // process (anything but reasonCode) makes the CRL unusable for ANY
1325
1489
  // certificate, not just the entry that carries it.
1326
1490
  for (var ry = 0; ry < theCrl.revokedCertificates.length && !unhandledCritical; ry++) {
1327
1491
  var ees = theCrl.revokedCertificates[ry].crlEntryExtensions || [];
1328
1492
  for (var ex = 0; ex < ees.length; ex++) {
1329
- // Key on the stable OID only a display name is registry-dependent
1493
+ // Key on the stable OID only -- a display name is registry-dependent
1330
1494
  // (a custom OID could be registered as "reasonCode"), so matching by
1331
1495
  // name would let an unhandled critical entry extension fail open.
1332
1496
  if (ees[ex].critical && ees[ex].oid !== OID_REASON_CODE) { unhandledCritical = true; break; }
@@ -1339,7 +1503,7 @@ function crlChecker(crls) {
1339
1503
  // cannot by itself establish "good" (full coverage is unconfirmed). But a
1340
1504
  // serial it LISTS is a genuine revocation of this certificate (serials are
1341
1505
  // unique per issuer), so such a CRL must still be consulted for revocation
1342
- // dropping it wholesale would let a revoked cert slip under softFail.
1506
+ // -- dropping it wholesale would let a revoked cert slip under softFail.
1343
1507
  var scopeRevocationOnly = false;
1344
1508
  var idpExt = null;
1345
1509
  for (var e = 0; e < theCrl.crlExtensions.length; e++) if (theCrl.crlExtensions[e].oid === OID_IDP) idpExt = theCrl.crlExtensions[e];
@@ -1347,18 +1511,18 @@ function crlChecker(crls) {
1347
1511
  var idp = decodeIdp(idpExt);
1348
1512
  if (idp.malformed) continue; // scope unknown -> unusable
1349
1513
  // An indirect CRL carries entries for other issuers keyed by the
1350
- // per-entry certificateIssuer attribute (not tracked here) matching
1514
+ // per-entry certificateIssuer attribute (not tracked here) -- matching
1351
1515
  // by serial alone could revoke the wrong cert or falsely cover it, so
1352
1516
  // treat an indirect CRL as unusable until certificateIssuer is honored.
1353
1517
  if (idp.indirect) continue;
1354
1518
  if (idp.onlyAttr) continue; // scoped to attribute certs, not this public-key cert
1355
- if (idp.onlyCa && !certIsCa) continue; // out of scope for this cert
1356
- if (idp.onlyUser && certIsCa) continue;
1519
+ if (idp.onlyCa && certIsCa !== true) continue; // out of scope (or CA-ness undeterminable)
1520
+ if (idp.onlyUser && certIsCa !== false) continue;
1357
1521
  if (idp.hasDistributionPoint || idp.onlySomeReasons) scopeRevocationOnly = true;
1358
1522
  }
1359
1523
  if (theCrl.thisUpdate > time) continue; // not yet valid
1360
- // A CRL with no nextUpdate has no bounded validity its currency
1361
- // cannot be confirmed (RFC 5280 §5.1.2.5 requires nextUpdate), so a
1524
+ // A CRL with no nextUpdate has no bounded validity -- its currency
1525
+ // cannot be confirmed (RFC 5280 sec. 5.1.2.5 requires nextUpdate), so a
1362
1526
  // replayed old CRL must not read "good". Treat it as unusable.
1363
1527
  if (!theCrl.nextUpdate || theCrl.nextUpdate < time) continue; // stale / no bound
1364
1528
 
@@ -1367,7 +1531,7 @@ function crlChecker(crls) {
1367
1531
 
1368
1532
  // The CRL is now authoritative + current + verified. An authoritative
1369
1533
  // delta blocks a "good" result (its base is not merged here) and can only
1370
- // reveal a revocation never establish "good" on its own.
1534
+ // reveal a revocation -- never establish "good" on its own.
1371
1535
  if (isDelta) { sawDelta = true; scopeRevocationOnly = true; }
1372
1536
 
1373
1537
  for (var r = 0; r < theCrl.revokedCertificates.length; r++) {
@@ -1375,15 +1539,15 @@ function crlChecker(crls) {
1375
1539
  if (entry.serialNumberHex !== cert.serialNumberHex) continue;
1376
1540
  // reasonCode removeFromCRL (8) means the entry was un-revoked. In a
1377
1541
  // DELTA this releases the serial from hold; because the base is not
1378
- // merged here, a definitive "revoked" is no longer possible for it a
1542
+ // merged here, a definitive "revoked" is no longer possible for it -- a
1379
1543
  // base CRL that still lists it must not override the delta removal.
1380
1544
  if (crlEntryReason(entry) === 8) { if (isDelta) sawDeltaRemoval = true; continue; }
1381
- // A revocation is effective as of its revocationDate (RFC 5280 §5.3).
1545
+ // A revocation is effective as of its revocationDate (RFC 5280 sec. 5.3).
1382
1546
  // In the DEFAULT present-time validation a listed serial is revoked
1383
- // regardless of that date a future revocationDate is post-dating or
1547
+ // regardless of that date -- a future revocationDate is post-dating or
1384
1548
  // clock skew and must NOT read good. Only under an EXPLICIT historical
1385
- // validation (opts.historicalMode) validating as of a past instant,
1386
- // e.g. a timestamped signature does an entry dated AFTER the
1549
+ // validation (opts.historicalMode) -- validating as of a past instant,
1550
+ // e.g. a timestamped signature -- does an entry dated AFTER the
1387
1551
  // validation time not yet apply.
1388
1552
  if (historical && entry.revocationDate instanceof Date && entry.revocationDate.getTime() > time.getTime()) continue;
1389
1553
  // Record the revocation but keep scanning: a delta removeFromCRL for the
@@ -1392,19 +1556,22 @@ function crlChecker(crls) {
1392
1556
  break;
1393
1557
  }
1394
1558
  // A partition-scoped CRL that did not list this serial does NOT prove the
1395
- // cert is unrevoked (another shard/reason may revoke it) only a
1559
+ // cert is unrevoked (another shard/reason may revoke it) -- only a
1396
1560
  // full-scope CRL can establish "good".
1397
1561
  if (!scopeRevocationOnly) sawAuthoritative = true; // covered this cert, not listed
1398
1562
  }
1399
1563
  // A delta released this serial from hold: without merging its base we cannot
1400
- // return a definitive revoked (else a released cert stays rejected) the
1564
+ // return a definitive revoked (else a released cert stays rejected) -- the
1401
1565
  // status is undetermined. This outranks a base CRL's revocation.
1402
1566
  if (sawDeltaRemoval) return { status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" };
1403
1567
  if (revokedResult) return revokedResult;
1404
1568
  // A delta CRL for this issuer was seen but cannot be merged with its base,
1405
- // so the current revocation picture is incomplete never report "good".
1569
+ // so the current revocation picture is incomplete -- never report "good".
1406
1570
  if (sawDelta) return { status: "unknown", reason: "a delta CRL cannot be evaluated without combining it with its base CRL, so the revocation status is undetermined" };
1407
1571
  if (sawAuthoritative) return { status: "good" };
1572
+ if (certScopeFault) {
1573
+ return { status: "unknown", reason: "no authoritative in-scope CRL covers this certificate; its basicConstraints extension is unreadable (" + certScopeFault + "), so scope-limited CRLs were skipped" };
1574
+ }
1408
1575
  return { status: "unknown", reason: "no authoritative in-scope CRL covers this certificate" };
1409
1576
  },
1410
1577
  };