@blamejs/pki 0.1.22 → 0.1.23
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 +35 -0
- package/README.md +16 -6
- package/index.js +16 -12
- package/lib/asn1-der.js +126 -68
- package/lib/constants.js +46 -21
- package/lib/ct.js +54 -39
- package/lib/framework-error.js +57 -34
- package/lib/oid.js +110 -59
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +49 -41
- package/lib/schema-attrcert.js +101 -64
- package/lib/schema-cmp.js +152 -131
- package/lib/schema-cms.js +621 -163
- package/lib/schema-crl.js +43 -21
- package/lib/schema-crmf.js +136 -79
- package/lib/schema-csr.js +63 -14
- package/lib/schema-engine.js +92 -43
- package/lib/schema-ocsp.js +101 -55
- package/lib/schema-pkcs12.js +80 -64
- package/lib/schema-pkcs8.js +12 -12
- package/lib/schema-pkix.js +283 -100
- package/lib/schema-smime.js +39 -39
- package/lib/schema-tsp.js +108 -59
- package/lib/schema-x509.js +36 -25
- package/lib/webcrypto.js +156 -22
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 arc
|
|
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
|
|
87
|
-
// must be omitted
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
117
|
-
// the RFC 9909
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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])
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
682
|
-
excludedSubtrees:
|
|
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
|
|
715
|
-
//
|
|
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
|
|
728
|
-
// 6.1.3(d)(1)(ii): create the node from a
|
|
729
|
-
//
|
|
730
|
-
//
|
|
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,
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
// (
|
|
935
|
-
// SHOULD-be-non-critical (
|
|
936
|
-
// is both anomalous and unprocessed
|
|
937
|
-
// than let, e.g., a mapping to/from anyPolicy slip past the
|
|
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
|
|
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` (anchor
|
|
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
|
|
982
|
-
* `
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
//
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
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
|
|
1215
|
-
try {
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1269
|
-
// RFC 5280
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
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); }
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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)
|
|
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 &&
|
|
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
|
|
1361
|
-
// cannot be confirmed (RFC 5280
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
1386
|
-
// e.g. a timestamped signature
|
|
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)
|
|
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)
|
|
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
|
|
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
|
};
|