@blamejs/pki 0.6.14 → 0.6.16
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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -62,23 +62,12 @@ var edwardsPoint = require("./edwards-point");
|
|
|
62
62
|
|
|
63
63
|
var PathError = errors.PathError;
|
|
64
64
|
function E(code, message, cause) { return new PathError(code, message, cause); }
|
|
65
|
-
// Every code placed into the public validate() verdict must be a path/* code.
|
|
66
|
-
// A direct DER decode (resolveDescriptor's asn1.decode, run outside the ns-
|
|
67
|
-
// wrapping schema engine) can throw a raw asn1/* Asn1Error; normalize any
|
|
68
|
-
// non-path error code to the given path/* fallback so an internal domain code
|
|
69
|
-
// never leaks into the documented verdict. The original error is kept as `error`.
|
|
70
65
|
function pathCode(e, fallback) {
|
|
71
66
|
return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
// Membership decided by comparison rather than by a prototype method, through the shared guard.
|
|
75
|
-
// Every use below is a RULE: whether a certificate's extended key usages cover the ones the caller
|
|
76
|
-
// required, whether a policy is already expected at a node, whether anyPolicy is in the user's
|
|
77
|
-
// initial set. See guard-list for what a replaced Array.prototype.indexOf did to this validator.
|
|
78
69
|
var contains = guard.list.contains;
|
|
79
70
|
var containsAll = guard.list.containsAll;
|
|
80
|
-
// RFC 5280 6.1.4(a): a policyMappings extension MUST NOT map to or from anyPolicy. The scan for a
|
|
81
|
-
// violating pair is the rule, so it routes through the guard for the same reason.
|
|
82
71
|
function mapsAnyPolicy(mappings) {
|
|
83
72
|
return guard.list.anyMatches(mappings, function (m) {
|
|
84
73
|
return !!m && (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy);
|
|
@@ -87,21 +76,7 @@ function mapsAnyPolicy(mappings) {
|
|
|
87
76
|
|
|
88
77
|
var NS = pkix.makeNS("path", PathError, oid);
|
|
89
78
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
90
|
-
// Validates a trust-anchor tuple's raw publicKey as a COMPLETE SubjectPublicKeyInfo (AlgorithmIdentifier +
|
|
91
|
-
// key BIT STRING), not just a decodable first OID -- a structurally-incomplete SPKI must fail closed at the
|
|
92
|
-
// anchor door, not slip through to a soft valid:false when key import later rejects it.
|
|
93
79
|
var ANCHOR_SPKI_SCHEMA = pkix.spki(NS);
|
|
94
|
-
// A trust anchor carries a CLOSED, producer-derived field set -- the shape pki.trust.parseCertdata documents
|
|
95
|
-
// and path validation + trust dedup consume: name, publicKey, algorithm, parameters, purposes, distrustAfter,
|
|
96
|
-
// subjectDer, label, mozillaCaPolicy. Normalizing a caller tuple reads each of those fields exactly once (by
|
|
97
|
-
// property access, which follows the prototype chain, so an inherited restriction is kept) and builds a fresh
|
|
98
|
-
// object from those reads -- publicKey pinned to a private copy, algorithm/parameters taken from the validated
|
|
99
|
-
// SubjectPublicKeyInfo (never the caller's declared value), the validation-read metadata (name, purposes,
|
|
100
|
-
// distrustAfter) materialized as DATA because validation reads each more than once and a live accessor could
|
|
101
|
-
// answer inconsistently. entry's own shape is NEVER enumerated (no `in` / ownKeys / getOwnPropertyDescriptor)
|
|
102
|
-
// and the prototype chain is never walked, so a Proxy anchor cannot use an enumeration trap to mutate state
|
|
103
|
-
// mid-normalization and a hostile getPrototypeOf cannot be driven into a loop (Buffer.isBuffer invokes
|
|
104
|
-
// getPrototypeOf once). A field the caller attached OUTSIDE the closed set is not carried. See toAnchor.
|
|
105
80
|
|
|
106
81
|
var subtle = webcrypto.webcrypto.subtle;
|
|
107
82
|
|
|
@@ -125,82 +100,34 @@ var OID = {
|
|
|
125
100
|
caIssuers: oid.byName("caIssuers"),
|
|
126
101
|
};
|
|
127
102
|
|
|
128
|
-
// The set of extension OIDs the validator processes. An unrecognized critical
|
|
129
|
-
// extension outside this set fails the path (6.1.4(o), 6.1.5(e)).
|
|
130
|
-
// extendedKeyUsage is recognized: the critical form is legal (4.2.1.12) and
|
|
131
|
-
// appears in the wild (RFC 6960 4.2.2.2 delegated OCSP responders); its
|
|
132
|
-
// structure is validated wherever it is critical, and key-purpose enforcement
|
|
133
|
-
// is the caller's opt-in via opts.requiredEku (RFC 5280 6.1 defines no EKU
|
|
134
|
-
// processing step -- the required purpose is application context).
|
|
135
103
|
var PROCESSED_EXTENSIONS = {};
|
|
136
|
-
// cRLDistributionPoints is processed: the CRL checker consults it for the
|
|
137
|
-
// sec. 6.3.3 shard correspondence, and a critical instance (sec. 4.2.1.13 is a
|
|
138
|
-
// SHOULD-non-critical) is structurally validated by
|
|
139
|
-
// validateCriticalExtensionStructure via the registered decoder. freshestCRL
|
|
140
|
-
// stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
|
|
141
|
-
// consult it (no delta merge), so a critical instance fails unrecognized.
|
|
142
|
-
// qcStatements is deliberately left unprocessed: a critical QC statement (QcLimitValue reliance limit,
|
|
143
|
-
// QcType certificate purpose) asserts qualified-certificate semantics a relying party MUST enforce, and
|
|
144
|
-
// this validator does not enforce them (nor expose a handler to). Marking it processed would let a caller
|
|
145
|
-
// treat a certificate as valid outside its asserted critical QC constraints, so a critical qcStatements
|
|
146
|
-
// fails as an unrecognized-critical extension (RFC 5280 sec. 6.1.4); a non-critical instance is
|
|
147
|
-
// informational and does not affect the verdict. The extension is still decoded for pki.inspect / lint.
|
|
148
104
|
[OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
|
|
149
105
|
OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
|
|
150
106
|
OID.extKeyUsage, OID.cRLDistributionPoints].
|
|
151
107
|
forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
|
|
152
|
-
// Frozen after seeding: this exact object is both consulted by the critical-extension check here and
|
|
153
|
-
// exported (for pki.lint to stay consistent). A caller must not be able to add an OID, because that
|
|
154
|
-
// would make an attacker's critical, decoder-less extension pass as "processed" and skip both the
|
|
155
|
-
// unrecognized-critical check and structural validation. Freezing makes any such write a no-op.
|
|
156
108
|
Object.freeze(PROCESSED_EXTENSIONS);
|
|
157
109
|
|
|
158
|
-
// ---- signature verify bridge (NEW 6) ---------------------------------------
|
|
159
110
|
|
|
160
|
-
// Signature-algorithm OID -> the WebCrypto verify descriptor + how to import
|
|
161
|
-
// the issuer SPKI. Keyed via oid.byName so no dotted-decimal OID literal
|
|
162
|
-
// appears in source (the registry owns arc<->name). The algorithm is a property
|
|
163
|
-
// of the CERTIFICATE and the issuer key, never of a message-selected field
|
|
164
|
-
// (CVE-2015-9235).
|
|
165
111
|
var SIG_ALGS = {};
|
|
166
|
-
// `params` is the REQUIRED AlgorithmIdentifier parameters shape: "null" (a DER
|
|
167
|
-
// NULL must be present -- RSASSA-PKCS1-v1_5, RFC 4055 sec. 5) or "absent" (parameters
|
|
168
|
-
// must be omitted -- ECDSA/EdDSA/ML-DSA, RFC 5758/8410). A cert deviating from
|
|
169
|
-
// its algorithm's required shape is malformed and rejected before verify.
|
|
170
|
-
// `sameKeyOid` marks the one-shot families whose public-key algorithm OID equals
|
|
171
|
-
// the signature algorithm OID: EdDSA, ML-DSA, SLH-DSA. For these, Node's
|
|
172
|
-
// WebCrypto imports an SPKI of another type under the requested name and verifies
|
|
173
|
-
// with the real key (unlike RSA/ECDSA, where a mismatched SPKI is refused at
|
|
174
|
-
// import), so the issuer-key <-> signature-algorithm consistency (RFC 9814 sec. 4)
|
|
175
|
-
// must be checked structurally: the SPKI OID must equal the signature OID.
|
|
176
112
|
function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
|
|
177
113
|
var entry = { verify: verify, imp: imp, params: params };
|
|
178
114
|
if (ecdsa) entry.ecdsa = true;
|
|
179
115
|
if (sameKeyOid) entry.sameKeyOid = true;
|
|
180
|
-
// EdDSA descriptors carry the Edwards curve id (6 = Ed25519, 7 = Ed448) so the verify path
|
|
181
|
-
// validates the issuer point through the shared gate without re-branching on the algorithm name.
|
|
182
116
|
if (verify.name === "Ed25519") entry.eddsa = 6;
|
|
183
117
|
else if (verify.name === "Ed448") entry.eddsa = 7;
|
|
184
118
|
SIG_ALGS[oid.byName(name)] = entry;
|
|
185
119
|
}
|
|
186
|
-
// RSASSA-PKCS1-v1_5 -- parameters MUST be NULL.
|
|
187
120
|
_sig("sha256WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, "null");
|
|
188
121
|
_sig("sha384WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, "null");
|
|
189
122
|
_sig("sha512WithRSAEncryption", { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, "null");
|
|
190
|
-
// ECDSA (hash in the OID; the curve comes from the imported key) -- params absent.
|
|
191
123
|
_sig("ecdsaWithSHA256", { name: "ECDSA", hash: "SHA-256" }, { name: "ECDSA" }, "absent", true);
|
|
192
124
|
_sig("ecdsaWithSHA384", { name: "ECDSA", hash: "SHA-384" }, { name: "ECDSA" }, "absent", true);
|
|
193
125
|
_sig("ecdsaWithSHA512", { name: "ECDSA", hash: "SHA-512" }, { name: "ECDSA" }, "absent", true);
|
|
194
|
-
// EdDSA (one-shot, no hash parameter) -- params absent; key OID == sig OID.
|
|
195
126
|
_sig("Ed25519", { name: "Ed25519" }, { name: "Ed25519" }, "absent", false, true);
|
|
196
127
|
_sig("Ed448", { name: "Ed448" }, { name: "Ed448" }, "absent", false, true);
|
|
197
|
-
// ML-DSA (FIPS 204) -- params absent; key OID == sig OID.
|
|
198
128
|
_sig("id-ml-dsa-44", { name: "ML-DSA-44" }, { name: "ML-DSA-44" }, "absent", false, true);
|
|
199
129
|
_sig("id-ml-dsa-65", { name: "ML-DSA-65" }, { name: "ML-DSA-65" }, "absent", false, true);
|
|
200
130
|
_sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", false, true);
|
|
201
|
-
// SLH-DSA (FIPS 205) -- params absent; key OID == sig OID. The twelve pure sets;
|
|
202
|
-
// the RFC 9909 sec. 3 OID name maps to the WebCrypto set name by id-slh-dsa-<set> ->
|
|
203
|
-
// SLH-DSA-<SET> (the webcrypto SLH_DSA_NODE keys). One-shot verify like ML-DSA.
|
|
204
131
|
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
205
132
|
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
206
133
|
].forEach(function (set) {
|
|
@@ -208,65 +135,36 @@ _sig("id-ml-dsa-87", { name: "ML-DSA-87" }, { name: "ML-DSA-87" }, "absent", fal
|
|
|
208
135
|
_sig("id-slh-dsa-" + set, { name: wc }, { name: wc }, "absent", false, true);
|
|
209
136
|
});
|
|
210
137
|
|
|
211
|
-
// RSASSA-PSS resolves its hash + salt from the AlgorithmIdentifier parameters.
|
|
212
138
|
var OID_RSA_PSS = oid.byName("rsassaPss");
|
|
213
139
|
var OID_MGF1 = oid.byName("mgf1");
|
|
214
|
-
// SHA-1 is deliberately ABSENT -- a SHA-1 signature (PKCS#1 or PSS) is rejected,
|
|
215
|
-
// matching the no-sha1WithRSAEncryption posture (SHAttered chosen-prefix).
|
|
216
140
|
var HASH_BY_OID = {};
|
|
217
141
|
HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
|
|
218
142
|
HASH_BY_OID[oid.byName("sha384")] = "SHA-384";
|
|
219
143
|
HASH_BY_OID[oid.byName("sha512")] = "SHA-512";
|
|
220
144
|
|
|
221
|
-
// The order-aware ECDSA DER->P1363 converter + its CURVE_FIELD_BYTES / CURVE_ORDER tables now
|
|
222
|
-
// live in validator-sig.js (validator.sig.ecdsaDerToP1363), shared with the composite engine.
|
|
223
145
|
|
|
224
|
-
// The algorithm OID of an AlgorithmIdentifier SEQUENCE { algorithm OID,
|
|
225
|
-
// parameters OPTIONAL }. STRICT: a universal SEQUENCE with the OID and AT MOST
|
|
226
|
-
// one optional parameters element -- a bare [n]-wrapped OID (no SEQUENCE) or a
|
|
227
|
-
// SEQUENCE carrying a spurious third element is malformed and must not be read
|
|
228
|
-
// leniently as its named algorithm.
|
|
229
146
|
function seqAlgOid(seq) {
|
|
230
147
|
if (!seq || seq.tagClass !== "universal" || seq.tagNumber !== asn1.TAGS.SEQUENCE || !seq.children || seq.children.length < 1 || seq.children.length > 2) {
|
|
231
148
|
throw E("path/unsupported-algorithm", "expected an AlgorithmIdentifier SEQUENCE { OID, parameters? }");
|
|
232
149
|
}
|
|
233
150
|
return asn1.read.oid(seq.children[0]);
|
|
234
151
|
}
|
|
235
|
-
// A hash AlgorithmIdentifier { OID, parameters? } whose parameters, when present,
|
|
236
|
-
// MUST be DER NULL (RFC 4055 sec. 2.1 / RFC 5754) -- never a SEQUENCE or arbitrary
|
|
237
|
-
// value. Used for the PSS hashAlgorithm and the MGF1 inner hash.
|
|
238
152
|
function hashAlgOid(seq) {
|
|
239
153
|
var o = seqAlgOid(seq);
|
|
240
154
|
if (seq.children.length === 2) {
|
|
241
155
|
var p = seq.children[1];
|
|
242
156
|
if (p.tagClass !== "universal" || p.tagNumber !== asn1.TAGS.NULL) throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier parameters must be NULL or absent (RFC 4055)");
|
|
243
|
-
// A NULL is well-formed only with empty content (X.690 sec. 8.8.2); the tag check
|
|
244
|
-
// alone would accept a non-empty NULL as valid parameters.
|
|
245
157
|
try { asn1.read.nullValue(p); }
|
|
246
158
|
catch (e) { throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier NULL parameters must have empty content (RFC 4055)", e); }
|
|
247
159
|
}
|
|
248
160
|
return o;
|
|
249
161
|
}
|
|
250
|
-
// The hash OID inside an EXPLICIT [n] wrapper around a hash AlgorithmIdentifier.
|
|
251
162
|
function explicitHashAlgOid(wrapper) {
|
|
252
|
-
// Coverage residual -- unreachable: the sole caller (resolveRsaPss) already
|
|
253
|
-
// asserts the EXPLICIT wrapper carries exactly one child before calling this, so
|
|
254
|
-
// this identical inner check cannot fire; it is a local defense-in-depth backstop.
|
|
255
163
|
if (!wrapper.children || wrapper.children.length !== 1) throw E("path/unsupported-algorithm", "malformed EXPLICIT hash AlgorithmIdentifier");
|
|
256
164
|
return hashAlgOid(wrapper.children[0]);
|
|
257
165
|
}
|
|
258
166
|
|
|
259
167
|
function resolveRsaPss(paramsBytes) {
|
|
260
|
-
// RSASSA-PSS-params ::= SEQUENCE { hashAlgorithm [0] DEFAULT sha1,
|
|
261
|
-
// maskGenAlgorithm [1] DEFAULT mgf1SHA1, saltLength [2] INTEGER DEFAULT 20,
|
|
262
|
-
// trailerField [3] DEFAULT 1 }. WebCrypto verifies with MGF1 keyed to the
|
|
263
|
-
// same hash as the signature and trailerField 0xBC (1). A declared value
|
|
264
|
-
// that deviates cannot be honored, so it is rejected; verifying it under
|
|
265
|
-
// WebCrypto's defaults would be a signatureAlgorithm bypass.
|
|
266
|
-
// RFC 4055 DEFAULTs are SHA-1 (hashAlgorithm and mgf1SHA1). Because SHA-1 is
|
|
267
|
-
// rejected, an absent hashAlgorithm or maskGenAlgorithm would resolve to SHA-1
|
|
268
|
-
// and must be REJECTED -- a supported PSS AlgorithmIdentifier must state both
|
|
269
|
-
// explicitly, with the MGF1 hash matching the signature hash.
|
|
270
168
|
var hash = null, saltLength = 20, mgfNode = null, trailer = 1;
|
|
271
169
|
if (!paramsBytes) throw E("path/unsupported-algorithm", "RSASSA-PSS requires explicit parameters (the SHA-1 defaults are rejected)");
|
|
272
170
|
var n = asn1.decode(paramsBytes);
|
|
@@ -276,34 +174,19 @@ function resolveRsaPss(paramsBytes) {
|
|
|
276
174
|
var pssLastTag = -1;
|
|
277
175
|
n.children.forEach(function (f) {
|
|
278
176
|
if (f.tagClass !== "context") throw E("path/unsupported-algorithm", "RSASSA-PSS-params fields must be context-tagged (RFC 4055)");
|
|
279
|
-
// Fields are the OPTIONAL [0..3], each at most once and in ascending DER
|
|
280
|
-
// order; an unknown or repeated/out-of-order tag is malformed.
|
|
281
177
|
if (f.tagNumber > 3 || f.tagNumber <= pssLastTag) throw E("path/unsupported-algorithm", "RSASSA-PSS-params has an unexpected, duplicate, or out-of-order field [" + f.tagNumber + "]");
|
|
282
178
|
pssLastTag = f.tagNumber;
|
|
283
|
-
// Every RSASSA-PSS-params field is an EXPLICIT [n] wrapper (constructed)
|
|
284
|
-
// around exactly one value (an AlgorithmIdentifier or an INTEGER); a
|
|
285
|
-
// primitive/childless or multi-child context field is malformed: reading
|
|
286
|
-
// f.children[0] and ignoring the rest would accept non-DER parameters.
|
|
287
179
|
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)");
|
|
288
180
|
if (f.tagNumber === 0) {
|
|
289
181
|
var h = explicitHashAlgOid(f);
|
|
290
182
|
if (!HASH_BY_OID[h]) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS hash algorithm " + h);
|
|
291
183
|
hash = HASH_BY_OID[h];
|
|
292
184
|
} else if (f.tagNumber === 1) {
|
|
293
|
-
mgfNode = f.children[0];
|
|
185
|
+
mgfNode = f.children[0];
|
|
294
186
|
} else if (f.tagNumber === 2) {
|
|
295
187
|
var sl = asn1.read.integer(f.children[0]);
|
|
296
|
-
// A negative saltLength must be rejected: the OpenSSL-backed verify shim
|
|
297
|
-
// reads -1/-2/-3 as RSA_PSS_SALTLEN_DIGEST/AUTO/MAX, and AUTO (-2) accepts
|
|
298
|
-
// a signature of ANY salt length -- defeating the salt-length binding.
|
|
299
|
-
// The upper bound keeps the value exact through Number conversion: the
|
|
300
|
-
// verifier binds to the salt length the certificate states, so a value
|
|
301
|
-
// that would round is not verifiable material (no real salt exceeds the
|
|
302
|
-
// modulus size, let alone this).
|
|
303
188
|
saltLength = guard.range.uint31(sl, E, "path/unsupported-algorithm", "RSASSA-PSS saltLength");
|
|
304
189
|
} else if (f.tagNumber === 3) {
|
|
305
|
-
// Compared for equality with 1 below -- bound before conversion so an
|
|
306
|
-
// oversized value cannot round on its way to the comparison.
|
|
307
190
|
trailer = guard.range.uint31(asn1.read.integer(f.children[0]), E, "path/unsupported-algorithm", "RSASSA-PSS trailerField");
|
|
308
191
|
}
|
|
309
192
|
});
|
|
@@ -318,42 +201,23 @@ function resolveRsaPss(paramsBytes) {
|
|
|
318
201
|
return { verify: { name: "RSA-PSS", saltLength: saltLength }, imp: { name: "RSA-PSS", hash: hash } };
|
|
319
202
|
}
|
|
320
203
|
|
|
321
|
-
// A DER NULL parameters field is the 2-byte 05 00.
|
|
322
204
|
function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
323
205
|
|
|
324
206
|
function resolveDescriptor(sigAlg) {
|
|
325
207
|
if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
|
|
326
|
-
// Composite ML-DSA: the OID-keyed registry + the parameters-absent check
|
|
327
|
-
// (draft-ietf-lamps-pq-composite-sigs sec. 5.3) live in composite-sig.js, shared with
|
|
328
|
-
// CMS. sameKeyOid enforces the RFC 9814 sec. 4 key<->signature OID consistency.
|
|
329
208
|
var comp = compositeSig.resolveCompositeDescriptor(sigAlg, PathError, "path/unsupported-algorithm");
|
|
330
209
|
if (comp) return comp;
|
|
331
210
|
var d = SIG_ALGS[sigAlg.oid];
|
|
332
211
|
if (!d) throw E("path/unsupported-algorithm", "no verify descriptor for signature algorithm " + (sigAlg.name || sigAlg.oid));
|
|
333
|
-
// The signatureAlgorithm's parameters MUST match the algorithm's fixed shape:
|
|
334
|
-
// RSASSA-PKCS1-v1_5 requires a NULL; ECDSA/EdDSA/ML-DSA require absence. A
|
|
335
|
-
// deviating AlgorithmIdentifier is malformed and must not verify.
|
|
336
212
|
var p = sigAlg.parameters;
|
|
337
213
|
if (d.params === "null" && !isDerNull(p)) throw E("path/unsupported-algorithm", "signature algorithm parameters must be NULL (RFC 4055)");
|
|
338
214
|
if (d.params === "absent" && p !== null && p !== undefined) throw E("path/unsupported-algorithm", "signature algorithm parameters must be absent (RFC 5758/8410)");
|
|
339
215
|
return d;
|
|
340
216
|
}
|
|
341
217
|
|
|
342
|
-
// RFC 9814 sec. 4 issuer-key <-> signature-algorithm consistency (algorithm-confusion
|
|
343
|
-
// defense). For the one-shot families whose public key shares the signature OID
|
|
344
|
-
// (EdDSA, ML-DSA, SLH-DSA), Node's WebCrypto imports an SPKI of a DIFFERENT type
|
|
345
|
-
// under the requested name and verifies with the real key, so an Ed25519-signed
|
|
346
|
-
// certificate labeled SLH-DSA would otherwise validate. Enforce structurally:
|
|
347
|
-
// the issuer SPKI's algorithm OID MUST equal the signature algorithm OID. (For
|
|
348
|
-
// RSA/ECDSA -- different key vs signature OIDs -- WebCrypto's import already rejects
|
|
349
|
-
// a mismatched key type, so `sameKeyOid` is not set and this is a no-op.)
|
|
350
218
|
function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
|
|
351
219
|
if (!d || !d.sameKeyOid) return;
|
|
352
220
|
var keyOid;
|
|
353
|
-
// The working-key SPKI is decodable on the shipped path (the anchor's is validated at toAnchor entry and
|
|
354
|
-
// every chain SPKI is strict-parsed), so this catch is defense-in-depth -- unreachable through
|
|
355
|
-
// pki.path.validate, kept to keep a typed verdict for any future caller that supplies raw key bytes. The
|
|
356
|
-
// RFC 9814 sec. 4 algorithm-confusion check below stays reachable and covered.
|
|
357
221
|
try { keyOid = asn1.read.oid(asn1.decode(spkiBytes).children[0].children[0]); }
|
|
358
222
|
catch (e) { throw E("path/algorithm-mismatch", "cannot read the issuer public-key algorithm identifier", e); }
|
|
359
223
|
if (keyOid !== sigOid) {
|
|
@@ -361,23 +225,8 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
|
|
|
361
225
|
}
|
|
362
226
|
}
|
|
363
227
|
|
|
364
|
-
// ecdsaDerToP1363 relocated to validator-sig.js; the composite trad-ECDSA and classical ECDSA
|
|
365
|
-
// paths call validator.sig.ecdsaDerToP1363(sig, curve, PathError, "path/bad-signature").
|
|
366
228
|
|
|
367
|
-
// ---- composite ML-DSA signatures (draft-ietf-lamps-pq-composite-sigs) -------
|
|
368
|
-
// The composite verify/sign engine + the COMPOSITE_ALGS OID-keyed registry live in
|
|
369
|
-
// composite-sig.js (shared with CMS composite SignerInfo). Path validation composes it:
|
|
370
|
-
// resolveDescriptor (above) delegates to compositeSig.resolveCompositeDescriptor, the
|
|
371
|
-
// certificate + OCSP verify paths to compositeSig.compositeVerify, and
|
|
372
|
-
// compositeKeyUsageCheck (below) enforces the sec. 5.2 signature-only keyUsage restriction.
|
|
373
229
|
|
|
374
|
-
// draft-ietf-lamps-pq-composite-sigs sec. 5.2: a certificate whose SubjectPublicKeyInfo
|
|
375
|
-
// carries a composite ML-DSA OID, if it has a keyUsage extension, MUST assert at least
|
|
376
|
-
// one signature bit (digitalSignature / nonRepudiation / keyCertSign / cRLSign) and
|
|
377
|
-
// MUST NOT assert any encryption or key-establishment bit. A composite ML-DSA key is a
|
|
378
|
-
// signature-only key (ML-DSA cannot encrypt or agree, so a "dual usage" key is forbidden
|
|
379
|
-
// even when the traditional component could encrypt). The caller invokes this only for a
|
|
380
|
-
// composite-keyed certificate; an absent keyUsage places no restriction (RFC 5280 4.2.1.3).
|
|
381
230
|
function compositeKeyUsageCheck(cert) {
|
|
382
231
|
var ku;
|
|
383
232
|
try { ku = decodeExt(cert, OID.keyUsage); }
|
|
@@ -395,17 +244,9 @@ function compositeKeyUsageCheck(cert) {
|
|
|
395
244
|
return { ok: true };
|
|
396
245
|
}
|
|
397
246
|
|
|
398
|
-
// The ML-KEM SubjectPublicKeyInfo OIDs (RFC 9935 / FIPS 203). A certificate carrying one of
|
|
399
|
-
// these keys is a KEM key-establishment certificate: it can neither sign nor agree.
|
|
400
247
|
var ML_KEM_OIDS = {};
|
|
401
248
|
["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { ML_KEM_OIDS[oid.byName(n)] = true; });
|
|
402
249
|
|
|
403
|
-
// RFC 9935 sec. 5: a certificate whose SubjectPublicKeyInfo carries an id-ml-kem-* OID, if it
|
|
404
|
-
// has a keyUsage extension, MUST assert keyEncipherment as the only key usage set, since an
|
|
405
|
-
// ML-KEM key is a key-establishment-only key (it cannot sign or agree, so no other bit is legitimate,
|
|
406
|
-
// and an unnamed/reserved bit set alongside keyEncipherment is equally forbidden). The caller
|
|
407
|
-
// invokes this only for an ML-KEM-keyed certificate; an absent keyUsage places no restriction
|
|
408
|
-
// (RFC 5280 sec. 4.2.1.3). This also makes an ML-KEM "CA" (keyCertSign) an explicit reject.
|
|
409
250
|
function kemKeyUsageCheck(cert) {
|
|
410
251
|
var ku;
|
|
411
252
|
try { ku = decodeExt(cert, OID.keyUsage); }
|
|
@@ -421,13 +262,6 @@ function kemKeyUsageCheck(cert) {
|
|
|
421
262
|
return { ok: true };
|
|
422
263
|
}
|
|
423
264
|
|
|
424
|
-
// Import a descriptor's verification key, validating an EdDSA point before the import runs:
|
|
425
|
-
// node/OpenSSL import a low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without
|
|
426
|
-
// complaint and such a key verifies a forged signature. Both the certificate path and the
|
|
427
|
-
// revocation (CRL / OCSP-response) path import through this single seam, so neither can skip
|
|
428
|
-
// the point gate; a low-order issuer / responder key fails the caller closed (a rejected
|
|
429
|
-
// promise the caller maps to a bad verdict), never verifying a forged chain or a forged
|
|
430
|
-
// revocation.
|
|
431
265
|
function _importVerifyKey(spkiBytes, d) {
|
|
432
266
|
try {
|
|
433
267
|
if (d.eddsa) edwardsPoint.validateSpki(spkiBytes, d.eddsa, PathError, "path/bad-signature");
|
|
@@ -435,19 +269,13 @@ function _importVerifyKey(spkiBytes, d) {
|
|
|
435
269
|
return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]);
|
|
436
270
|
}
|
|
437
271
|
|
|
438
|
-
// Verify cert.signatureValue over cert.tbsBytes with the working public key.
|
|
439
272
|
function builtinVerify(state, cert) {
|
|
440
273
|
var d;
|
|
441
274
|
try {
|
|
442
275
|
d = resolveDescriptor(cert.signatureAlgorithm);
|
|
443
276
|
assertKeyMatchesSigAlg(state.workingPublicKey, cert.signatureAlgorithm.oid, d);
|
|
444
277
|
} catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
|
|
445
|
-
// The signature is an octet-aligned BIT STRING (no unused bits) for every
|
|
446
|
-
// supported algorithm; a non-zero unused-bit count is malformed.
|
|
447
278
|
if (!guard.crypto.isOctetAligned(cert.signatureValue)) return Promise.resolve({ ok: false, code: "path/bad-signature" });
|
|
448
|
-
// A composite signature verifies its ML-DSA and traditional halves and accepts
|
|
449
|
-
// IFF both pass -- delegated to the composite combinator (which reuses this
|
|
450
|
-
// file's ECDSA range-check + the same import/verify seam).
|
|
451
279
|
if (d.composite) return compositeSig.compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature");
|
|
452
280
|
var key;
|
|
453
281
|
return _importVerifyKey(state.workingPublicKey, d).then(function (k) {
|
|
@@ -458,20 +286,11 @@ function builtinVerify(state, cert) {
|
|
|
458
286
|
}).then(function (ok) {
|
|
459
287
|
return { ok: ok === true };
|
|
460
288
|
}, function (e) {
|
|
461
|
-
// A raw OpenSSL / WebCrypto fault (wrong key type for the declared
|
|
462
|
-
// algorithm -- the algorithm-confusion case) is a signature failure, not a
|
|
463
|
-
// path/* verdict of its own; only a PathError code is preserved.
|
|
464
289
|
return { ok: false, code: pathCode(e, "path/bad-signature"), error: e };
|
|
465
290
|
});
|
|
466
291
|
}
|
|
467
292
|
|
|
468
|
-
// ---- 7.1 name comparison ---------------------------------------------------
|
|
469
293
|
|
|
470
|
-
// RFC 5280 sec. 7.1 canonical DN / RDN comparison, via the shared name guard: the
|
|
471
|
-
// canonical form (case-fold + internal-whitespace collapse) and the embedded
|
|
472
|
-
// control-byte reject (CVE-2009-2408 -> path/name-chaining) live once in
|
|
473
|
-
// guard-name, so no path-validation caller can reintroduce a raw-byte DN
|
|
474
|
-
// comparison that treats two RFC 5280-equal names as different.
|
|
475
294
|
function dnEqual(rdnsA, rdnsB) {
|
|
476
295
|
return guard.name.dnEqual(rdnsA, rdnsB, E, "path/name-chaining", "distinguished name");
|
|
477
296
|
}
|
|
@@ -480,7 +299,6 @@ function rdnEqual(a, b) {
|
|
|
480
299
|
return guard.name.rdnEqual(a, b, E, "path/name-chaining", "distinguished name");
|
|
481
300
|
}
|
|
482
301
|
|
|
483
|
-
// ---- extension access ------------------------------------------------------
|
|
484
302
|
|
|
485
303
|
function findExt(cert, extOid) {
|
|
486
304
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
@@ -489,7 +307,6 @@ function findExt(cert, extOid) {
|
|
|
489
307
|
return null;
|
|
490
308
|
}
|
|
491
309
|
|
|
492
|
-
// Decode an extension value, mapping the typed decoder throw to a check.
|
|
493
310
|
function decodeExt(cert, extOid) {
|
|
494
311
|
var ext = findExt(cert, extOid);
|
|
495
312
|
if (!ext) return null;
|
|
@@ -497,12 +314,6 @@ function decodeExt(cert, extOid) {
|
|
|
497
314
|
return { critical: ext.critical, value: dec(ext.value) };
|
|
498
315
|
}
|
|
499
316
|
|
|
500
|
-
// RFC 5280 requires several CA-scoped extensions to be marked critical:
|
|
501
|
-
// basicConstraints (4.2.1.9), nameConstraints (4.2.1.10),
|
|
502
|
-
// policyConstraints (4.2.1.11), inhibitAnyPolicy (4.2.1.14). A conforming
|
|
503
|
-
// validator rejects the non-critical form -- an extension a non-supporting
|
|
504
|
-
// relying party would ignore must not silently pass here either. Returns a
|
|
505
|
-
// typed PathError when a PRESENT extension is not critical, else null.
|
|
506
317
|
function requireCriticalExt(ext, name, checks) {
|
|
507
318
|
if (ext && ext.critical !== true) {
|
|
508
319
|
checks.push({ name: name, ok: false, code: "path/extension-not-critical" });
|
|
@@ -511,14 +322,7 @@ function requireCriticalExt(ext, name, checks) {
|
|
|
511
322
|
return null;
|
|
512
323
|
}
|
|
513
324
|
|
|
514
|
-
// ---- name constraints ------------------------------------------------------
|
|
515
325
|
|
|
516
|
-
// Split an addr-spec into [localPart, host] at its single "@". Returns null when
|
|
517
|
-
// there is no "@" (a bare host/domain constraint), or "ambiguous" when there are
|
|
518
|
-
// multiple "@": a conformant certificate rfc822Name is a simple addr-spec with
|
|
519
|
-
// exactly one "@" (RFC 5280 sec. 4.2.1.6 deprecates the quoted local part, and an
|
|
520
|
-
// addr-spec domain never contains "@"), so a multi-"@" mailbox like
|
|
521
|
-
// "a@b"@example.com cannot be split reliably and must fail closed.
|
|
522
326
|
function splitMailbox(addr) {
|
|
523
327
|
var first = addr.indexOf("@");
|
|
524
328
|
if (first === -1) return null;
|
|
@@ -527,24 +331,14 @@ function splitMailbox(addr) {
|
|
|
527
331
|
}
|
|
528
332
|
|
|
529
333
|
function emailMatch(constraint, mailbox) {
|
|
530
|
-
// RFC 5280 sec. 4.2.1.10 rfc822Name: a constraint with an "@" is a full mailbox;
|
|
531
|
-
// a leading "." is a domain matching mailboxes at a SUBDOMAIN; otherwise it is
|
|
532
|
-
// a host matching mailboxes AT that host only. RFC 5321: the local part is
|
|
533
|
-
// CASE-SENSITIVE (exact); only the host is compared case-insensitively.
|
|
534
334
|
var mb = splitMailbox(mailbox);
|
|
535
|
-
if (mb === "ambiguous") return "unsupported";
|
|
335
|
+
if (mb === "ambiguous") return "unsupported";
|
|
536
336
|
if (constraint.indexOf("@") !== -1) {
|
|
537
|
-
// Full-mailbox constraint: exact local part + case-insensitive host. The
|
|
538
|
-
// host is canonicalized like dNSName/URI (strip the absolute-FQDN root dot)
|
|
539
|
-
// so a trailing-dot mailbox cannot escape the constraint.
|
|
540
337
|
var cb = splitMailbox(constraint);
|
|
541
338
|
if (cb === "ambiguous" || cb === null || mb === null) return "unsupported";
|
|
542
339
|
return mb[0] === cb[0] && stripTrailingDot(mb[1].toLowerCase()) === stripTrailingDot(cb[1].toLowerCase());
|
|
543
340
|
}
|
|
544
|
-
|
|
545
|
-
// trailing FQDN root dot stripped on both sides (as hostConstraintMatch does)
|
|
546
|
-
// so "user@evil.com." does not slip a constraint on "evil.com".
|
|
547
|
-
if (mb === null) return "unsupported"; // no host -> cannot determine domain
|
|
341
|
+
if (mb === null) return "unsupported";
|
|
548
342
|
var host = stripTrailingDot(mb[1].toLowerCase());
|
|
549
343
|
if (host === "") return "unsupported";
|
|
550
344
|
var c = stripTrailingDot(constraint.toLowerCase());
|
|
@@ -552,63 +346,44 @@ function emailMatch(constraint, mailbox) {
|
|
|
552
346
|
return host === c;
|
|
553
347
|
}
|
|
554
348
|
|
|
555
|
-
// Strip a single trailing dot (the absolute-FQDN root label) so "evil.com."
|
|
556
|
-
// and "evil.com" compare equal. Without that, a trailing-dot SAN would escape
|
|
557
|
-
// a dNSName constraint.
|
|
558
349
|
function stripTrailingDot(s) { return s.charAt(s.length - 1) === "." ? s.slice(0, -1) : s; }
|
|
559
350
|
|
|
560
|
-
// Host-suffix match with the RFC 5280 sec. 4.2.1.10 leading-period rule shared by
|
|
561
|
-
// dNSName and uniformResourceIdentifier constraints on a host.
|
|
562
351
|
function hostConstraintMatch(constraint, host) {
|
|
563
352
|
var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
|
|
564
353
|
if (c === "") return true;
|
|
565
|
-
if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
|
|
566
|
-
return h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Is `host` a fully qualified domain name (as a URI host constraint requires,
|
|
570
|
-
// RFC 5280 sec. 4.2.1.10)? A dotless single label (localhost), an IPv4/IPv6 literal,
|
|
571
|
-
// or a value carrying non-hostname characters (a scheme "://", a path "/", a
|
|
572
|
-
// port ":") is not a FQDN and cannot be matched against a domain-suffix
|
|
573
|
-
// constraint. Only [A-Za-z0-9.-] with at least one dot qualifies.
|
|
574
|
-
// A code-point membership table built from a string of the allowed characters, and the shared scan that
|
|
575
|
-
// checks every character of a non-empty string against one -- replacing the hostname-charset regexes
|
|
576
|
-
// through one loop (no per-predicate character-class chain).
|
|
354
|
+
if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
|
|
355
|
+
return h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c);
|
|
356
|
+
}
|
|
357
|
+
|
|
577
358
|
function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
|
|
578
|
-
var _FQDN_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-");
|
|
579
|
-
var _DIGDOT_TABLE = _charTable("0123456789.");
|
|
359
|
+
var _FQDN_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-");
|
|
360
|
+
var _DIGDOT_TABLE = _charTable("0123456789.");
|
|
580
361
|
function _allCharsIn(s, table) {
|
|
581
362
|
if (s.length === 0) return false;
|
|
582
363
|
for (var i = 0; i < s.length; i++) if (!table[_charCodeAt(s, i)]) return false;
|
|
583
364
|
return true;
|
|
584
365
|
}
|
|
585
|
-
// /^[0-9]/ : begins with an ASCII digit (a dotted-OID attempt, vs an EKU / purpose name).
|
|
586
366
|
function _startsWithDigit(s) { if (s.length === 0) return false; var c = _charCodeAt(s, 0); return c >= 48 && c <= 57; }
|
|
587
367
|
|
|
588
|
-
var _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-");
|
|
589
|
-
// /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i : the [start, end) range of the authority (up to the first / ? #
|
|
590
|
-
// or end), or null when the input is not a scheme "//" URI. Returns a range so the caller slices once
|
|
591
|
-
// (the slice that replaces the removed .exec), not a fresh substring here.
|
|
368
|
+
var _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-");
|
|
592
369
|
function _uriAuthorityRange(uri) {
|
|
593
370
|
var n = uri.length;
|
|
594
371
|
if (n === 0) return null;
|
|
595
372
|
var c0 = _charCodeAt(uri, 0);
|
|
596
|
-
if (!((c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122))) return null;
|
|
373
|
+
if (!((c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122))) return null;
|
|
597
374
|
var p = 1;
|
|
598
375
|
while (p < n && _SCHEME_TABLE[_charCodeAt(uri, p)]) p += 1;
|
|
599
|
-
if (_charCodeAt(uri, p) !== 58 || _charCodeAt(uri, p + 1) !== 47 || _charCodeAt(uri, p + 2) !== 47) return null;
|
|
376
|
+
if (_charCodeAt(uri, p) !== 58 || _charCodeAt(uri, p + 1) !== 47 || _charCodeAt(uri, p + 2) !== 47) return null;
|
|
600
377
|
p += 3;
|
|
601
378
|
var authStart = p;
|
|
602
|
-
while (p < n) { var d = _charCodeAt(uri, p); if (d === 47 || d === 63 || d === 35) break; p += 1; }
|
|
379
|
+
while (p < n) { var d = _charCodeAt(uri, p); if (d === 47 || d === 63 || d === 35) break; p += 1; }
|
|
603
380
|
return [authStart, p];
|
|
604
381
|
}
|
|
605
|
-
// /:\d+$/ : the index at which a trailing ":" + digits port starts (to slice off), or s.length when absent.
|
|
606
382
|
function _portStripIndex(s) {
|
|
607
383
|
var i = s.length;
|
|
608
384
|
while (i > 0) { var c = _charCodeAt(s, i - 1); if (c >= 48 && c <= 57) i -= 1; else break; }
|
|
609
385
|
return (i < s.length && i > 0 && _charCodeAt(s, i - 1) === 58) ? i - 1 : s.length;
|
|
610
386
|
}
|
|
611
|
-
// /^\[(.*)\]$/ -> "$1" : an IPv6 literal's surrounding "[" "]" removed, else the string unchanged.
|
|
612
387
|
function _stripBrackets(s) {
|
|
613
388
|
var n = s.length;
|
|
614
389
|
return (n >= 2 && _charCodeAt(s, 0) === 91 && _charCodeAt(s, n - 1) === 93) ? s.slice(1, n - 1) : s;
|
|
@@ -616,25 +391,17 @@ function _stripBrackets(s) {
|
|
|
616
391
|
|
|
617
392
|
function isFqdnHost(host) {
|
|
618
393
|
var h = stripTrailingDot(host);
|
|
619
|
-
if (h === "" || h.indexOf(".") === -1) return false;
|
|
620
|
-
if (!_allCharsIn(h, _FQDN_TABLE)) return false;
|
|
621
|
-
if (_allCharsIn(h, _DIGDOT_TABLE)) return false;
|
|
394
|
+
if (h === "" || h.indexOf(".") === -1) return false;
|
|
395
|
+
if (!_allCharsIn(h, _FQDN_TABLE)) return false;
|
|
396
|
+
if (_allCharsIn(h, _DIGDOT_TABLE)) return false;
|
|
622
397
|
return true;
|
|
623
398
|
}
|
|
624
399
|
|
|
625
|
-
// A URI constraint applies to the host part: a leading "." matches subdomains
|
|
626
|
-
// only; a bare host matches that host exactly (not subdomains, per sec. 4.2.1.10).
|
|
627
|
-
// Both sides must be a fully qualified domain name. A URI SAN with no host / an
|
|
628
|
-
// IP literal, or a malformed constraint that is not an FQDN (e.g. a full URI
|
|
629
|
-
// "http://blocked.example" where the required form is "blocked.example"), cannot
|
|
630
|
-
// be evaluated, so it returns "unsupported" and the caller fails closed; reporting
|
|
631
|
-
// it as a plain non-match would let the name silently escape a critical constraint.
|
|
632
400
|
function uriMatch(constraint, uri) {
|
|
633
401
|
var host = uriHost(uri);
|
|
634
402
|
if (host === null) return "unsupported";
|
|
635
403
|
if (!isFqdnHost(host)) return "unsupported";
|
|
636
404
|
var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
|
|
637
|
-
// Validate the constraint's own host form (strip a single leading "." domain marker).
|
|
638
405
|
if (!isFqdnHost(c.charAt(0) === "." ? c.slice(1) : c)) return "unsupported";
|
|
639
406
|
if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
|
|
640
407
|
return h === c;
|
|
@@ -644,20 +411,14 @@ function uriHost(uri) {
|
|
|
644
411
|
var r = _uriAuthorityRange(uri);
|
|
645
412
|
if (r === null) return null;
|
|
646
413
|
var authority = uri.slice(r[0], r[1]);
|
|
647
|
-
// authority = [ userinfo "@" ] host [ ":" port ]. RFC 3986 userinfo does not
|
|
648
|
-
// contain a raw "@", so a conformant authority has at most one "@". With more
|
|
649
|
-
// than one the host cannot be determined reliably, so this fails closed
|
|
650
|
-
// (null -> uriMatch returns "unsupported" -> the caller rejects the path);
|
|
651
|
-
// guessing a host could slip a URI name constraint.
|
|
652
414
|
var firstAt = authority.indexOf("@");
|
|
653
415
|
if (firstAt !== authority.lastIndexOf("@")) return null;
|
|
654
416
|
if (firstAt !== -1) authority = authority.slice(firstAt + 1);
|
|
655
417
|
var host = authority.slice(0, _portStripIndex(authority));
|
|
656
|
-
return host === "" ? null : host;
|
|
418
|
+
return host === "" ? null : host;
|
|
657
419
|
}
|
|
658
420
|
|
|
659
421
|
function ipMatch(constraint, addr) {
|
|
660
|
-
// constraint = address+mask (8 or 32 octets); addr = 4 or 16.
|
|
661
422
|
var half = constraint.length / 2;
|
|
662
423
|
if (addr.length !== half) return false;
|
|
663
424
|
for (var i = 0; i < half; i++) {
|
|
@@ -666,25 +427,18 @@ function ipMatch(constraint, addr) {
|
|
|
666
427
|
return true;
|
|
667
428
|
}
|
|
668
429
|
|
|
669
|
-
// Does GeneralName-like {tagNumber, value} match a constraint base of the same
|
|
670
|
-
// form? Returns true/false for a supported form, null when the forms differ
|
|
671
|
-
// (not comparable), or the string "unsupported" when the forms match but this
|
|
672
|
-
// validator does not implement that form's comparison. The caller must then fail
|
|
673
|
-
// closed: an unenforceable constraint cannot be read as "no match".
|
|
674
430
|
function nameMatchesConstraint(gnTag, gnValue, base) {
|
|
675
|
-
if (base.tagNumber !== gnTag) return null;
|
|
431
|
+
if (base.tagNumber !== gnTag) return null;
|
|
676
432
|
switch (gnTag) {
|
|
677
|
-
case 1: return emailMatch(base.value, gnValue);
|
|
678
|
-
case 2: return hostConstraintMatch(base.value, gnValue);
|
|
679
|
-
case 6: return uriMatch(base.value, gnValue);
|
|
680
|
-
case 7: return ipMatch(base.value, gnValue);
|
|
681
|
-
case 4: return dnStartsWith(gnValue, base.value);
|
|
682
|
-
default: return "unsupported";
|
|
433
|
+
case 1: return emailMatch(base.value, gnValue);
|
|
434
|
+
case 2: return hostConstraintMatch(base.value, gnValue);
|
|
435
|
+
case 6: return uriMatch(base.value, gnValue);
|
|
436
|
+
case 7: return ipMatch(base.value, gnValue);
|
|
437
|
+
case 4: return dnStartsWith(gnValue, base.value);
|
|
438
|
+
default: return "unsupported";
|
|
683
439
|
}
|
|
684
440
|
}
|
|
685
441
|
|
|
686
|
-
// directoryName constraint: the name DN must contain the constraint DN as an
|
|
687
|
-
// initial RDN sequence.
|
|
688
442
|
function dnStartsWith(nameDn, constraintDn) {
|
|
689
443
|
if (constraintDn.rdns.length > nameDn.rdns.length) return false;
|
|
690
444
|
for (var i = 0; i < constraintDn.rdns.length; i++) {
|
|
@@ -693,31 +447,16 @@ function dnStartsWith(nameDn, constraintDn) {
|
|
|
693
447
|
return true;
|
|
694
448
|
}
|
|
695
449
|
|
|
696
|
-
// Collect the name forms a cert presents for constraint checking: its SAN
|
|
697
|
-
// entries plus (per 4.2.1.10 / 6.1.3) an emailAddress in the subject DN as an
|
|
698
|
-
// rfc822Name, plus the subject DN itself as a directoryName.
|
|
699
450
|
function certNameForms(cert) {
|
|
700
451
|
var forms = [];
|
|
701
452
|
var san = decodeExt(cert, OID.subjectAltName);
|
|
702
453
|
var hasRfc822San = false;
|
|
703
454
|
if (san) {
|
|
704
|
-
// Preserve every SAN entry, including a form whose value the validator does
|
|
705
|
-
// not decode (x400Address [3] / ediPartyName [5]); dropping one would let a
|
|
706
|
-
// critical constraint of that same unsupported form pass unenforced. The
|
|
707
|
-
// constraint check fails such a form closed (name-constraint-unsupported).
|
|
708
455
|
san.value.names.forEach(function (nm) {
|
|
709
|
-
if (nm.tagNumber === 1) hasRfc822San = true;
|
|
710
|
-
// Coverage residual -- the `undefined -> null` arm is unreachable: the SAN
|
|
711
|
-
// decoder (schema-pkix altName, decodeValue:true) routes through generalName,
|
|
712
|
-
// which already maps an undecoded value to null, so nm.value is never undefined.
|
|
456
|
+
if (nm.tagNumber === 1) hasRfc822San = true;
|
|
713
457
|
forms.push({ tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
|
|
714
458
|
});
|
|
715
459
|
}
|
|
716
|
-
// RFC 5280 sec. 4.2.1.10: the legacy emailAddress in the subject DN is checked as
|
|
717
|
-
// an rfc822Name unless the SAN already carries the email identity as an
|
|
718
|
-
// rfc822Name entry. A SAN of a different form (e.g. dNSName only) does not
|
|
719
|
-
// cover the email, so the subject-DN email must still be constrained. Otherwise
|
|
720
|
-
// an excluded/non-permitted mailbox would slip an rfc822Name constraint.
|
|
721
460
|
if (!hasRfc822San) {
|
|
722
461
|
cert.subject.rdns.forEach(function (rdn) {
|
|
723
462
|
rdn.forEach(function (atv) {
|
|
@@ -729,18 +468,8 @@ function certNameForms(cert) {
|
|
|
729
468
|
return forms;
|
|
730
469
|
}
|
|
731
470
|
|
|
732
|
-
// Check a cert's own names against the accumulated constraints. `excluded` is
|
|
733
|
-
// a union (any match rejects). `permitted` is the intersection of every
|
|
734
|
-
// absorbing cert's permittedSubtrees (6.1.4(g)): the permitted set is tracked
|
|
735
|
-
// as one generation per absorbing cert, and a name of form F must match a
|
|
736
|
-
// subtree of form F in every generation that constrains form F. A flat pool
|
|
737
|
-
// would compute the union, letting a subordinate CA broaden what its parent
|
|
738
|
-
// permitted (a name-constraint bypass).
|
|
739
471
|
function checkNameConstraints(state, cert) {
|
|
740
472
|
var forms = certNameForms(cert);
|
|
741
|
-
// Excluded: any match -> reject. A constraint of the same form the validator
|
|
742
|
-
// cannot compare ("unsupported") means a critical exclusion it cannot
|
|
743
|
-
// enforce, so it fails closed on the same footing as a match.
|
|
744
473
|
for (var e = 0; e < state.excludedSubtrees.length; e++) {
|
|
745
474
|
var ex = state.excludedSubtrees[e];
|
|
746
475
|
for (var i = 0; i < forms.length; i++) {
|
|
@@ -749,14 +478,11 @@ function checkNameConstraints(state, cert) {
|
|
|
749
478
|
if (m === "unsupported") return { ok: false, code: "path/name-constraint-unsupported" };
|
|
750
479
|
}
|
|
751
480
|
}
|
|
752
|
-
// Permitted: for each name form, every generation that constrains that form
|
|
753
|
-
// must admit it (intersection across generations). If the only subtrees of
|
|
754
|
-
// that form are unenforceable, the name cannot be confirmed permitted.
|
|
755
481
|
for (var f = 0; f < forms.length; f++) {
|
|
756
482
|
var nf = forms[f];
|
|
757
483
|
for (var g = 0; g < state.permittedGenerations.length; g++) {
|
|
758
484
|
var formSubtrees = state.permittedGenerations[g].filter(function (s) { return s.tag === nf.tag; });
|
|
759
|
-
if (!formSubtrees.length) continue;
|
|
485
|
+
if (!formSubtrees.length) continue;
|
|
760
486
|
var permitted = false, unsupported = false;
|
|
761
487
|
formSubtrees.forEach(function (s) {
|
|
762
488
|
var r = nameMatchesConstraint(nf.tag, nf.value, { tagNumber: s.tag, value: s.base });
|
|
@@ -769,9 +495,6 @@ function checkNameConstraints(state, cert) {
|
|
|
769
495
|
return { ok: true };
|
|
770
496
|
}
|
|
771
497
|
|
|
772
|
-
// Absorb a cert's nameConstraints (6.1.4(g)): permittedSubtrees becomes a new
|
|
773
|
-
// GENERATION (intersection is enforced at check time by requiring a match in
|
|
774
|
-
// every generation); excludedSubtrees union into the flat excluded pool.
|
|
775
498
|
function absorbNameConstraints(state, decoded) {
|
|
776
499
|
if (decoded.permittedSubtrees.length) {
|
|
777
500
|
state.permittedGenerations.push(decoded.permittedSubtrees.map(function (st) {
|
|
@@ -783,15 +506,10 @@ function absorbNameConstraints(state, decoded) {
|
|
|
783
506
|
});
|
|
784
507
|
}
|
|
785
508
|
|
|
786
|
-
// ---- certificate-policy tree ----------------------------------------------
|
|
787
509
|
|
|
788
510
|
function rootNode() {
|
|
789
511
|
return { depth: 0, validPolicy: OID.anyPolicy, qualifierSet: [], expectedPolicySet: [OID.anyPolicy], children: [], parent: null };
|
|
790
512
|
}
|
|
791
|
-
// A deep copy of the valid-policy tree that omits the internal `parent` back-pointer,
|
|
792
|
-
// so the structured verdict returned to callers is acyclic (JSON.stringify-safe).
|
|
793
|
-
// The `parent` link is an implementation detail of the 6.1.3 processing, not part
|
|
794
|
-
// of the RFC 5280 valid_policy_tree a consumer inspects.
|
|
795
513
|
function treeWithoutParent(node) {
|
|
796
514
|
if (!node) return null;
|
|
797
515
|
return {
|
|
@@ -804,9 +522,7 @@ function treeWithoutParent(node) {
|
|
|
804
522
|
}
|
|
805
523
|
function leavesAt(tree, depth) {
|
|
806
524
|
var out = [];
|
|
807
|
-
|
|
808
|
-
// truthy before calling leavesAt; the null-tree early return is a backstop.
|
|
809
|
-
if (!tree) return out; // a pruned-empty tree has no nodes
|
|
525
|
+
if (!tree) return out;
|
|
810
526
|
(function walk(node) {
|
|
811
527
|
if (node.depth === depth) { out.push(node); return; }
|
|
812
528
|
node.children.forEach(walk);
|
|
@@ -814,7 +530,6 @@ function leavesAt(tree, depth) {
|
|
|
814
530
|
return out;
|
|
815
531
|
}
|
|
816
532
|
function pruneChildless(tree, depth) {
|
|
817
|
-
// Delete depth-`depth` nodes with no children, then propagate upward.
|
|
818
533
|
for (var d = depth; d > 0; d--) {
|
|
819
534
|
leavesAt(tree, d).forEach(function (node) {
|
|
820
535
|
if (node.children.length === 0 && node.parent) {
|
|
@@ -826,24 +541,16 @@ function pruneChildless(tree, depth) {
|
|
|
826
541
|
}
|
|
827
542
|
function treeIsEmpty(tree) { return tree.children.length === 0; }
|
|
828
543
|
|
|
829
|
-
// ---- the state machine -----------------------------------------------------
|
|
830
544
|
|
|
831
|
-
// Is `base` the value shape the constraint matcher compares for a GeneralName
|
|
832
|
-
// form? Forms 0/3/5/8 (otherName / x400Address / ediPartyName / registeredID)
|
|
833
|
-
// carry any present value -- the matcher fails them closed as unsupported.
|
|
834
545
|
function isSubtreeBaseValid(tag, base) {
|
|
835
546
|
switch (tag) {
|
|
836
|
-
case 1: case 2: case 6: return typeof base === "string";
|
|
837
|
-
case 7: return Buffer.isBuffer(base) && (base.length === 8 || base.length === 32);
|
|
838
|
-
case 4: return base !== null && typeof base === "object" && !!base.rdns && intrinsic.isArray(base.rdns);
|
|
547
|
+
case 1: case 2: case 6: return typeof base === "string";
|
|
548
|
+
case 7: return Buffer.isBuffer(base) && (base.length === 8 || base.length === 32);
|
|
549
|
+
case 4: return base !== null && typeof base === "object" && !!base.rdns && intrinsic.isArray(base.rdns);
|
|
839
550
|
default: return base !== undefined;
|
|
840
551
|
}
|
|
841
552
|
}
|
|
842
553
|
|
|
843
|
-
// Entry-point validation of a 6.1.1(b,c) user-initial subtree seed. A
|
|
844
|
-
// mis-shaped entry (e.g. the { base: { tagNumber, value } } shape the
|
|
845
|
-
// nameConstraints decoder emits) would never match any name, silently
|
|
846
|
-
// disabling the constraint the caller configured -- so it throws instead.
|
|
847
554
|
function checkedSubtreeSeeds(list, optName) {
|
|
848
555
|
if (list === undefined || list === null) return [];
|
|
849
556
|
if (!intrinsic.isArray(list)) throw E("path/bad-input", "validate: opts." + optName + " must be an array of { tag, base } subtree entries");
|
|
@@ -851,9 +558,6 @@ function checkedSubtreeSeeds(list, optName) {
|
|
|
851
558
|
if (!st || typeof st !== "object" || !Number.isInteger(st.tag) || st.tag < 0 || st.tag > 8) {
|
|
852
559
|
throw E("path/bad-input", "validate: opts." + optName + " entries must be { tag: <GeneralName tag number 0..8>, base: <that form's constraint value> }");
|
|
853
560
|
}
|
|
854
|
-
// An iPAddress base (address+mask) may arrive as any BufferSource; normalize it to a
|
|
855
|
-
// private Buffer so the matcher can index it by byte and a caller cannot mutate it once a
|
|
856
|
-
// microtask has passed. The name forms (string / directoryName) are validated as they stand.
|
|
857
561
|
var base = (st.tag === 7 && guard.bytes.isByteSource(st.base))
|
|
858
562
|
? guard.bytes.snapshotSource(st.base, PathError, "path/bad-input", "an iPAddress subtree base")
|
|
859
563
|
: st.base;
|
|
@@ -870,8 +574,6 @@ function initialize(certs, params, seeds, anchor) {
|
|
|
870
574
|
validPolicyTree: rootNode(),
|
|
871
575
|
policyNodeCount: 1,
|
|
872
576
|
maxPolicyNodes: params.maxPolicyNodes !== undefined ? params.maxPolicyNodes : constants.LIMITS.PATH_MAX_POLICY_NODES,
|
|
873
|
-
// Each absorbing cert's permittedSubtrees is one generation; a name must be
|
|
874
|
-
// admitted by every generation (intersection). An initial seed is generation 0.
|
|
875
577
|
permittedGenerations: seeds.permitted.length ? [seeds.permitted] : [],
|
|
876
578
|
excludedSubtrees: seeds.excluded,
|
|
877
579
|
explicitPolicy: params.initialExplicitPolicy ? 0 : n + 1,
|
|
@@ -887,10 +589,6 @@ function initialize(certs, params, seeds, anchor) {
|
|
|
887
589
|
};
|
|
888
590
|
}
|
|
889
591
|
|
|
890
|
-
// self-issued = subject DN equals issuer DN. dnEqual throws on a NUL/control
|
|
891
|
-
// DN (CVE-2009-2408); a malformed-DN cert is never "self-issued" (and is failed
|
|
892
|
-
// by the name-chaining check), so the throw is swallowed here, keeping it from
|
|
893
|
-
// rejecting the whole validate() promise at these unwrapped call sites.
|
|
894
592
|
function selfIssued(cert) {
|
|
895
593
|
try { return dnEqual(cert.subject.rdns, cert.issuer.rdns); }
|
|
896
594
|
catch (_e) { return false; }
|
|
@@ -904,11 +602,6 @@ function processPolicies(state, cert, i, checks) {
|
|
|
904
602
|
if (cp && state.validPolicyTree) {
|
|
905
603
|
var policies = cp.value;
|
|
906
604
|
var depth = i - 1;
|
|
907
|
-
// anyPolicy processing is active only while inhibit_anyPolicy > 0, or for a
|
|
908
|
-
// self-issued non-final cert. What the gate covers is the (d)(2) expansion of
|
|
909
|
-
// a cert-asserted anyPolicy and nothing more. 4.2.1.14 inhibition rests entirely on
|
|
910
|
-
// that gate: a depth-(i-1) anyPolicy node created while processing was
|
|
911
|
-
// active remains matchable in (d)(1)(ii).
|
|
912
605
|
var anyPolicyActive = state.inhibitAnyPolicy > 0 || (i < state._n && selfIssued(cert));
|
|
913
606
|
var anyPolicyPresent = false;
|
|
914
607
|
var anyPolicyQualifiers = null;
|
|
@@ -922,19 +615,12 @@ function processPolicies(state, cert, i, checks) {
|
|
|
922
615
|
}
|
|
923
616
|
});
|
|
924
617
|
if (!matched) {
|
|
925
|
-
// 6.1.3(d)(1)(ii): with no expected-policy match, create the node from a
|
|
926
|
-
// depth-(i-1) anyPolicy node. The RFC runs this step UNCONDITIONALLY
|
|
927
|
-
// (no inhibit clause); gating it would false-reject a path whose
|
|
928
|
-
// specific policy chains through a legitimately created anyPolicy node.
|
|
929
618
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
930
619
|
if (node.validPolicy === OID.anyPolicy) addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
|
|
931
620
|
});
|
|
932
621
|
}
|
|
933
622
|
});
|
|
934
623
|
if (anyPolicyPresent && anyPolicyActive) {
|
|
935
|
-
// 6.1.3(d)(2): expand anyPolicy into unmatched expected-policy values.
|
|
936
|
-
// Each generated child carries AP-Q -- the qualifier set of the
|
|
937
|
-
// certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
|
|
938
624
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
939
625
|
node.expectedPolicySet.forEach(function (ep) {
|
|
940
626
|
var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
|
|
@@ -949,7 +635,6 @@ function processPolicies(state, cert, i, checks) {
|
|
|
949
635
|
state.validPolicyTree = null;
|
|
950
636
|
}
|
|
951
637
|
|
|
952
|
-
// 6.1.3(f): interim check.
|
|
953
638
|
if (!(state.explicitPolicy > 0 || state.validPolicyTree !== null)) {
|
|
954
639
|
checks.push({ name: "policy", ok: false, code: "path/policy-required" });
|
|
955
640
|
return { fatal: true, error: E("path/policy-required", "explicit policy required but the valid-policy tree is empty") };
|
|
@@ -965,16 +650,10 @@ function addChild(state, parent, validPolicy, qualifiers, expectedPolicySet, che
|
|
|
965
650
|
void checks;
|
|
966
651
|
}
|
|
967
652
|
|
|
968
|
-
// A DER explicit NULL (05 00) as an AlgorithmIdentifier parameters field is
|
|
969
|
-
// treated identically to omitted parameters (RFC 5280 6.1.4(e)).
|
|
970
653
|
function isNullOrAbsentParams(p) {
|
|
971
654
|
return p === null || p === undefined || (p.length === 2 && p[0] === 0x05 && p[1] === 0x00);
|
|
972
655
|
}
|
|
973
656
|
|
|
974
|
-
// Rebuild a SubjectPublicKeyInfo with the given AlgorithmIdentifier parameters
|
|
975
|
-
// spliced in, so a key that inherited its domain parameters (an EC public key
|
|
976
|
-
// whose SPKI omits the namedCurve, RFC 5280 6.1.4(f)/6.1.5(d)) becomes a
|
|
977
|
-
// self-contained SPKI that importKey("spki", ...) can consume.
|
|
978
657
|
function spliceSpkiParameters(spki, algOid, paramsBytes) {
|
|
979
658
|
return asn1.build.sequence([
|
|
980
659
|
asn1.build.sequence([asn1.build.oid(algOid), asn1.build.raw(paramsBytes)]),
|
|
@@ -982,10 +661,6 @@ function spliceSpkiParameters(spki, algOid, paramsBytes) {
|
|
|
982
661
|
]);
|
|
983
662
|
}
|
|
984
663
|
|
|
985
|
-
// RFC 5280 6.1.4(d,e,f) / 6.1.5(c,d): set working_public_key / _algorithm /
|
|
986
|
-
// _parameters from a certificate. Present non-null parameters are copied;
|
|
987
|
-
// NULL-or-absent parameters inherit the prior parameters iff the key algorithm
|
|
988
|
-
// is unchanged, else clear them.
|
|
989
664
|
function updateWorkingKey(state, cert) {
|
|
990
665
|
var keyAlg = cert.subjectPublicKeyInfo.algorithm;
|
|
991
666
|
if (!isNullOrAbsentParams(keyAlg.parameters)) {
|
|
@@ -993,9 +668,6 @@ function updateWorkingKey(state, cert) {
|
|
|
993
668
|
} else if (keyAlg.oid !== state.workingPublicKeyAlgorithm) {
|
|
994
669
|
state.workingPublicKeyParameters = null;
|
|
995
670
|
}
|
|
996
|
-
// When this cert's SPKI omits its algorithm parameters but the working set
|
|
997
|
-
// carries inherited ones, store a reconstructed SPKI: the next signature verify
|
|
998
|
-
// needs a complete key and would fail on the bare bytes.
|
|
999
671
|
if (isNullOrAbsentParams(keyAlg.parameters) && state.workingPublicKeyParameters) {
|
|
1000
672
|
state.workingPublicKey = spliceSpkiParameters(cert.subjectPublicKeyInfo, keyAlg.oid, state.workingPublicKeyParameters);
|
|
1001
673
|
} else {
|
|
@@ -1004,18 +676,12 @@ function updateWorkingKey(state, cert) {
|
|
|
1004
676
|
state.workingPublicKeyAlgorithm = keyAlg.oid;
|
|
1005
677
|
}
|
|
1006
678
|
|
|
1007
|
-
// RFC 5280 4.2.1.12 -- when the caller states required key purposes, a cert
|
|
1008
|
-
// carrying an extendedKeyUsage must assert every one (or anyExtendedKeyUsage).
|
|
1009
|
-
// Applied to the TARGET cert (its own purposes) AND to every intermediate CA
|
|
1010
|
-
// (EKU chaining: an EKU on a CA constrains the purposes below it), so marking
|
|
1011
|
-
// extKeyUsage a processed critical extension is sound. The semantic gate runs
|
|
1012
|
-
// wherever the extension appears, never only on the leaf. A cert with no EKU is
|
|
1013
|
-
// unconstrained. Returns true if the cert FAILS the required-purpose check.
|
|
1014
679
|
function ekuPurposeFails(cert, requiredEku, checks) {
|
|
1015
680
|
var eku;
|
|
1016
681
|
try { eku = decodeExt(cert, OID.extKeyUsage); }
|
|
682
|
+
// allow:fail-open-verify -- ekuPurposeFails returns true to mean the EKU purpose FAILS (a rejecting verdict); a bad extension is recorded in checks and the purpose is treated as failed, which is fail-closed
|
|
1017
683
|
catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
|
|
1018
|
-
if (!eku) return false;
|
|
684
|
+
if (!eku) return false;
|
|
1019
685
|
var purposes = eku.value;
|
|
1020
686
|
var ok = contains(purposes, OID.anyExtendedKeyUsage) || containsAll(purposes, requiredEku);
|
|
1021
687
|
checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
|
|
@@ -1025,31 +691,22 @@ function ekuPurposeFails(cert, requiredEku, checks) {
|
|
|
1025
691
|
function prepareNext(state, cert, i, checks) {
|
|
1026
692
|
var isSelfIssued = selfIssued(cert);
|
|
1027
693
|
|
|
1028
|
-
// RFC 5280 4.2.1.12 EKU chaining -- an intermediate CA's EKU constrains the
|
|
1029
|
-
// purposes of the certs beneath it. Enforced here so a critical EKU on an
|
|
1030
|
-
// intermediate (now a PROCESSED extension) is not merely tolerated but honored.
|
|
1031
694
|
if (state.requiredEku && ekuPurposeFails(cert, state.requiredEku, checks)) {
|
|
1032
695
|
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)") };
|
|
1033
696
|
}
|
|
1034
697
|
|
|
1035
|
-
// (a),(b) policy mappings.
|
|
1036
698
|
var pm;
|
|
1037
699
|
try { pm = decodeExt(cert, OID.policyMappings); }
|
|
1038
700
|
catch (e) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
|
|
1039
701
|
if (pm) {
|
|
1040
|
-
// The scan for a forbidden anyPolicy mapping is the rule itself (RFC 5280 6.1.4(a)), so it is a
|
|
1041
|
-
// loop rather than a `some`: one answering false makes the forbidden mapping invisible and the
|
|
1042
|
-
// certificate is accepted carrying it.
|
|
1043
702
|
var badAny = mapsAnyPolicy(pm.value);
|
|
1044
703
|
if (badAny) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: E("path/bad-policy", "policyMappings must not map to or from anyPolicy (RFC 5280 6.1.4(a))") }; }
|
|
1045
704
|
if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
|
|
1046
705
|
}
|
|
1047
706
|
|
|
1048
|
-
// (c) working issuer name; (d),(e),(f) working key + algorithm + parameters.
|
|
1049
707
|
state.workingIssuerName = cert.subject;
|
|
1050
708
|
updateWorkingKey(state, cert);
|
|
1051
709
|
|
|
1052
|
-
// (g) name constraints absorb, once this cert's own names have been checked.
|
|
1053
710
|
var nc;
|
|
1054
711
|
try { nc = decodeExt(cert, OID.nameConstraints); }
|
|
1055
712
|
catch (e) { checks.push({ name: "nameConstraints", ok: false, code: pathCode(e, "path/bad-name-constraints") }); return { fatal: true, error: e }; }
|
|
@@ -1057,14 +714,12 @@ function prepareNext(state, cert, i, checks) {
|
|
|
1057
714
|
if (ncCritErr) return { fatal: true, error: ncCritErr };
|
|
1058
715
|
if (nc) absorbNameConstraints(state, nc.value);
|
|
1059
716
|
|
|
1060
|
-
// (h) decrement counters for a non-self-issued cert.
|
|
1061
717
|
if (!isSelfIssued) {
|
|
1062
718
|
if (state.explicitPolicy > 0) state.explicitPolicy--;
|
|
1063
719
|
if (state.policyMapping > 0) state.policyMapping--;
|
|
1064
720
|
if (state.inhibitAnyPolicy > 0) state.inhibitAnyPolicy--;
|
|
1065
721
|
}
|
|
1066
722
|
|
|
1067
|
-
// (i),(j) policy/inhibit clamps.
|
|
1068
723
|
var pc;
|
|
1069
724
|
try { pc = decodeExt(cert, OID.policyConstraints); }
|
|
1070
725
|
catch (e) { checks.push({ name: "policyConstraints", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
|
|
@@ -1081,7 +736,6 @@ function prepareNext(state, cert, i, checks) {
|
|
|
1081
736
|
if (iapCritErr) return { fatal: true, error: iapCritErr };
|
|
1082
737
|
if (iap && iap.value < state.inhibitAnyPolicy) state.inhibitAnyPolicy = iap.value;
|
|
1083
738
|
|
|
1084
|
-
// (k) basicConstraints cA gate, the single authoritative CA check.
|
|
1085
739
|
var bc;
|
|
1086
740
|
try { bc = decodeExt(cert, OID.basicConstraints); }
|
|
1087
741
|
catch (e) { checks.push({ name: "basicConstraints", ok: false, code: "path/bad-basic-constraints" }); return { fatal: true, error: e }; }
|
|
@@ -1089,19 +743,14 @@ function prepareNext(state, cert, i, checks) {
|
|
|
1089
743
|
checks.push({ name: "basicConstraints", ok: false, code: "path/not-a-ca" });
|
|
1090
744
|
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))") };
|
|
1091
745
|
}
|
|
1092
|
-
// 4.2.1.9: a CA certificate used to validate certificate signatures MUST mark
|
|
1093
|
-
// basicConstraints critical. A non-critical cA:TRUE is non-conforming. A
|
|
1094
|
-
// relying party that skips non-critical extensions would not see the CA bit.
|
|
1095
746
|
var bcCritErr = requireCriticalExt(bc, "basicConstraints", checks);
|
|
1096
747
|
if (bcCritErr) return { fatal: true, error: bcCritErr };
|
|
1097
|
-
// (l),(m) path length.
|
|
1098
748
|
if (!isSelfIssued) {
|
|
1099
749
|
if (state.maxPathLength <= 0) { checks.push({ name: "pathLength", ok: false, code: "path/path-length-exceeded" }); return { fatal: true, error: E("path/path-length-exceeded", "certification path is longer than the CA path-length constraint allows") }; }
|
|
1100
750
|
state.maxPathLength--;
|
|
1101
751
|
}
|
|
1102
752
|
if (bc.value.pathLenConstraint !== null && bc.value.pathLenConstraint < state.maxPathLength) state.maxPathLength = bc.value.pathLenConstraint;
|
|
1103
753
|
|
|
1104
|
-
// (n) keyUsage.keyCertSign.
|
|
1105
754
|
var ku;
|
|
1106
755
|
try { ku = decodeExt(cert, OID.keyUsage); }
|
|
1107
756
|
catch (e) { checks.push({ name: "keyUsage", ok: false, code: "path/bad-key-usage" }); return { fatal: true, error: e }; }
|
|
@@ -1115,12 +764,7 @@ function prepareNext(state, cert, i, checks) {
|
|
|
1115
764
|
function applyPolicyMappings(state, mappings, i) {
|
|
1116
765
|
var depth = i;
|
|
1117
766
|
if (state.policyMapping > 0) {
|
|
1118
|
-
|
|
1119
|
-
// the extension maps, REPLACE its expected_policy_set with the SET of
|
|
1120
|
-
// subjectDomainPolicy values mapped from that ID-P. Appending would retain
|
|
1121
|
-
// the pre-mapping policy, letting a later cert satisfy the chain by
|
|
1122
|
-
// asserting the mapped-away policy.
|
|
1123
|
-
var mappedFrom = {}; // issuerDomainPolicy -> [subjectDomainPolicy, ...]
|
|
767
|
+
var mappedFrom = {};
|
|
1124
768
|
mappings.forEach(function (m) { (mappedFrom[m.issuerDomainPolicy] = mappedFrom[m.issuerDomainPolicy] || []).push(m.subjectDomainPolicy); });
|
|
1125
769
|
var depthI = leavesAt(state.validPolicyTree, depth);
|
|
1126
770
|
var anyNodes = depthI.filter(function (nd) { return nd.validPolicy === OID.anyPolicy; });
|
|
@@ -1129,24 +773,14 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1129
773
|
if (idpNodes.length) {
|
|
1130
774
|
idpNodes.forEach(function (nd) { nd.expectedPolicySet = mappedFrom[idp].slice(); });
|
|
1131
775
|
} else {
|
|
1132
|
-
// 6.1.4(b)(1): with no depth-i ID-P node but a depth-i anyPolicy node, generate
|
|
1133
|
-
// the missing ID-P node under the anyPolicy node's parent with the mapped
|
|
1134
|
-
// expected set (else an anyPolicy-only CA loses the mapping).
|
|
1135
776
|
anyNodes.forEach(function (anyNode) {
|
|
1136
777
|
if (anyNode.parent) addChild(state, anyNode.parent, idp, anyNode.qualifierSet[0] || null, mappedFrom[idp].slice(), []);
|
|
1137
778
|
});
|
|
1138
779
|
}
|
|
1139
780
|
});
|
|
1140
781
|
} else {
|
|
1141
|
-
// 6.1.4(b)(2), policy_mapping == 0: delete every depth-i node whose
|
|
1142
|
-
// valid_policy is a mapped ID-P, then prune. A prior mapping in the same
|
|
1143
|
-
// extension may have already emptied the tree, so stop if it is gone.
|
|
1144
782
|
var mappedSet = {};
|
|
1145
783
|
mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
|
|
1146
|
-
// Coverage residual -- unreachable today: applyPolicyMappings is invoked once per
|
|
1147
|
-
// certificate under an `if (state.validPolicyTree)` guard and does not null the
|
|
1148
|
-
// tree before this point. Retained as a cheap correctness backstop for a future
|
|
1149
|
-
// per-mapping-batch refactor; do not remove.
|
|
1150
784
|
if (!state.validPolicyTree) return;
|
|
1151
785
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
1152
786
|
if (mappedSet[node.validPolicy] && node.parent) {
|
|
@@ -1154,30 +788,15 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1154
788
|
if (idx !== -1) node.parent.children.splice(idx, 1);
|
|
1155
789
|
}
|
|
1156
790
|
});
|
|
1157
|
-
// Prune from the PARENT depth (i-1): a pass starting at depth i would
|
|
1158
|
-
// delete the surviving unmapped depth-i leaves (all leaves are childless),
|
|
1159
|
-
// wrongly emptying the tree and false-rejecting a valid path.
|
|
1160
791
|
pruneChildless(state.validPolicyTree, depth - 1);
|
|
1161
792
|
if (treeIsEmpty(state.validPolicyTree)) state.validPolicyTree = null;
|
|
1162
793
|
}
|
|
1163
794
|
}
|
|
1164
795
|
|
|
1165
|
-
// policyMappings is semantically processed only in the prepare-for-next step
|
|
1166
|
-
// (sec. 6.1.4(a),(b)), which does not run for the target certificate. It is also
|
|
1167
|
-
// SHOULD-be-non-critical (sec. 4.2.1.5), so a critical policyMappings on the target
|
|
1168
|
-
// is both anomalous and unprocessed, and must fail closed (sec. 6.1.5(f)); accepting
|
|
1169
|
-
// it would let, e.g., a mapping to/from anyPolicy slip past the sec. 6.1.4(a) rejection
|
|
1170
|
-
// the intermediate path applies. (nameConstraints / inhibitAnyPolicy are also
|
|
1171
|
-
// prepare-next-only but are MUST-be-critical CA extensions, so a critical one on
|
|
1172
|
-
// a terminal CA cert is conforming and is not treated as unprocessed here.)
|
|
1173
796
|
var TARGET_UNPROCESSED_IF_CRITICAL = {};
|
|
1174
797
|
TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
|
|
1175
798
|
Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
|
|
1176
799
|
|
|
1177
|
-
// Membership is asked of the table's OWN properties. Freezing the table stops a write to it and
|
|
1178
|
-
// stops nothing about the prototype chain: a name planted on Object.prototype answers for every OID
|
|
1179
|
-
// the table does not carry, and read live this reported an unrecognized critical extension as
|
|
1180
|
-
// processed. On a CA certificate only the first table is consulted, so the chain was accepted.
|
|
1181
800
|
function unrecognizedCriticalExtension(cert, isTarget) {
|
|
1182
801
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
1183
802
|
var ext = cert.extensions[i];
|
|
@@ -1188,15 +807,12 @@ function unrecognizedCriticalExtension(cert, isTarget) {
|
|
|
1188
807
|
return null;
|
|
1189
808
|
}
|
|
1190
809
|
|
|
1191
|
-
// Decode every RECOGNIZED critical extension to enforce that its extnValue is
|
|
1192
|
-
// structurally valid -- even where the semantic gate is skipped (the leaf).
|
|
1193
|
-
// Returns the failing typed code, or null.
|
|
1194
810
|
function validateCriticalExtensionStructure(cert) {
|
|
1195
811
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
1196
812
|
var ext = cert.extensions[i];
|
|
1197
813
|
if (!ext.critical) continue;
|
|
1198
814
|
var dec = EXT_DECODERS[ext.oid];
|
|
1199
|
-
if (!dec) continue;
|
|
815
|
+
if (!dec) continue;
|
|
1200
816
|
try { dec(ext.value); }
|
|
1201
817
|
catch (e) { return pathCode(e, "path/bad-extension-value"); }
|
|
1202
818
|
}
|
|
@@ -1270,14 +886,6 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
1270
886
|
* });
|
|
1271
887
|
* res.valid; // boolean; res.results[0].checks carries the per-check codes
|
|
1272
888
|
*/
|
|
1273
|
-
// Every option pki.path.validate reads. The anchor option is `trustAnchors` (plural), the same
|
|
1274
|
-
// spelling pki.path.build / pki.cms.verify / pki.cmp.verify use, and it takes a single anchor or a
|
|
1275
|
-
// non-empty array of them. A one-element array reproduces the single-anchor behavior exactly.
|
|
1276
|
-
// Derived from three sources, because no one of them is complete: the `opts.*` reads in this
|
|
1277
|
-
// function, the `params.*` reads in the RFC 5280 6.1.1 state it is passed down to, and the
|
|
1278
|
-
// documented prose above, which is the only place `requiredEku` and the three initial-policy
|
|
1279
|
-
// inputs appear together. A grep of one function would have shipped a list missing four real
|
|
1280
|
-
// options and rejected callers that work today.
|
|
1281
889
|
var _VALIDATE_OPTS = {
|
|
1282
890
|
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
|
|
1283
891
|
initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
|
|
@@ -1286,11 +894,6 @@ var _VALIDATE_OPTS = {
|
|
|
1286
894
|
userInitialPolicySet: 1, verifier: 1
|
|
1287
895
|
};
|
|
1288
896
|
|
|
1289
|
-
// The opt-in Certificate Transparency gate (RFC 6962). When opts.ctLogList is supplied, the target's
|
|
1290
|
-
// embedded SCTs are verified against the trusted log-list and the caller's CT policy, and the verdict is
|
|
1291
|
-
// recorded on the target's checks. Fail-closed: a target that declares a CT policy but carries no
|
|
1292
|
-
// SCT-list extension is refused. Reached only through opts.ctLogList, so every existing caller is
|
|
1293
|
-
// unaffected. The SCT-list extension OID is looked up through the shared registry, never a literal.
|
|
1294
897
|
var OID_SCT_LIST = oid.byName("signedCertificateTimestampList");
|
|
1295
898
|
async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
|
|
1296
899
|
var exts = leaf.extensions || [], sctExt = null;
|
|
@@ -1298,8 +901,6 @@ async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
|
|
|
1298
901
|
if (exts[i] && (exts[i].oid === OID_SCT_LIST || exts[i].name === "signedCertificateTimestampList")) { sctExt = exts[i]; break; }
|
|
1299
902
|
}
|
|
1300
903
|
if (sctExt == null) return { name: "ct", ok: false, code: "path/ct-required" };
|
|
1301
|
-
// The leaf's issuer contributes issuerKeyHash: the certificate that signed it (certs[n-2] in the
|
|
1302
|
-
// anchor->target order), or the anchor itself when the target is validated directly beneath it.
|
|
1303
904
|
var issuer = n >= 2 ? certs[n - 2] : { subjectPublicKeyInfo: { bytes: anchor.publicKey } };
|
|
1304
905
|
var entry, list;
|
|
1305
906
|
try {
|
|
@@ -1308,12 +909,6 @@ async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
|
|
|
1308
909
|
} catch (e) {
|
|
1309
910
|
return { name: "ct", ok: false, code: pathCode(e, "path/ct-required") };
|
|
1310
911
|
}
|
|
1311
|
-
// verifySctList throws only for a mis-shaped caller configuration (a bad ctPolicy threshold value, a
|
|
1312
|
-
// ctLogList that is not a parseLogList result): that is the caller's error, surfaced at the path
|
|
1313
|
-
// boundary as path/bad-input rather than leaking the ct/* domain code of a layer they did not call.
|
|
1314
|
-
// verifySctList throws only for a mis-shaped caller configuration (a bad ctPolicy threshold value, a
|
|
1315
|
-
// ctLogList that is not a parseLogList result): that is the caller's error, surfaced at the path
|
|
1316
|
-
// boundary as path/bad-input rather than leaking the ct/* domain code of a layer they did not call.
|
|
1317
912
|
var verdict;
|
|
1318
913
|
try {
|
|
1319
914
|
verdict = await ct.verifySctList(entry, list, ctLogList,
|
|
@@ -1329,15 +924,7 @@ async function validate(path, opts) {
|
|
|
1329
924
|
opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
|
|
1330
925
|
guard.identifier.assertKnownKeys(opts, _VALIDATE_OPTS, E, "path/bad-input",
|
|
1331
926
|
"pki.path.validate has an unknown option (the anchor option is `trustAnchors`, a single anchor or an array). The unknown option was: ");
|
|
1332
|
-
// A CT policy without a log-list cannot be enforced -- refuse it at entry rather than silently
|
|
1333
|
-
// skipping the CT gate and returning a valid path the caller believes was CT-checked (fail-closed).
|
|
1334
927
|
if (opts.ctPolicy != null && opts.ctLogList == null) throw E("path/bad-input", "validate: opts.ctPolicy requires opts.ctLogList (the trusted CT log-list to verify the certificate's SCTs against)");
|
|
1335
|
-
// Validate the CT policy's KEYS at entry: a misspelled key (e.g. `minSCTs`) would otherwise read as
|
|
1336
|
-
// undefined and silently default to the RFC floor of 1, applying a weaker policy than the caller asked
|
|
1337
|
-
// for. The values are validated by verifySctList when the gate runs.
|
|
1338
|
-
// Validate the whole CT configuration at entry, independent of whether the target carries an SCT
|
|
1339
|
-
// extension, so a malformed policy or log-list is always path/bad-input -- never masked as a
|
|
1340
|
-
// per-certificate path/ct-required just because the target happens to lack SCTs.
|
|
1341
928
|
if (opts.ctLogList != null) {
|
|
1342
929
|
if (typeof opts.ctLogList !== "object" || opts.ctLogList.byLogId == null) throw E("path/bad-input", "validate: opts.ctLogList must be a pki.ct.parseLogList result");
|
|
1343
930
|
var ctp = guard.identifier.optionsObject(opts.ctPolicy, E, "path/bad-input", "validate: opts.ctPolicy");
|
|
@@ -1346,39 +933,17 @@ async function validate(path, opts) {
|
|
|
1346
933
|
if (ctp.minOperators != null && (typeof ctp.minOperators !== "number" || !intrinsic.isInteger(ctp.minOperators) || ctp.minOperators < 1)) throw E("path/bad-input", "validate: opts.ctPolicy.minOperators must be a positive integer");
|
|
1347
934
|
}
|
|
1348
935
|
if (!intrinsic.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
|
|
1349
|
-
// Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
|
|
1350
|
-
// (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
|
|
1351
|
-
// crypto amplification from an oversized path). Entry-point tier: throw.
|
|
1352
936
|
var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
1353
937
|
if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
|
|
1354
|
-
// Through the same door `build` uses, and for the same reason: every certificate this walk decides
|
|
1355
|
-
// over is re-derived from the bytes its parser read (see coerceCert), so a rebuilt object cannot
|
|
1356
|
-
// present a genuine signed range alongside substituted fields.
|
|
1357
938
|
var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
|
|
1358
939
|
var n = certs.length;
|
|
1359
940
|
if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
|
|
1360
|
-
// trustAnchors is a single anchor OR a non-empty array of them. Normalize each at the door: a parsed
|
|
1361
|
-
// root CERTIFICATE becomes a { name, publicKey, algorithm } tuple, and a mis-shaped tuple fails closed
|
|
1362
|
-
// with path/bad-input rather than seeding an undefined working key a self-describing SPKI could still
|
|
1363
|
-
// validate against (a soft-verdict fail-open). With more than one anchor, select the one whose subject
|
|
1364
|
-
// issued the top path cert (certs[0], anchor->target order); a one-element list reproduces the
|
|
1365
|
-
// single-anchor behavior exactly, and no match falls to the first so the name-chaining check below
|
|
1366
|
-
// fails closed (an untrusted chain is never validated against a non-matching root).
|
|
1367
941
|
var rawAnchors = opts.trustAnchors;
|
|
1368
942
|
var anchorList = intrinsic.isArray(rawAnchors) ? rawAnchors : [rawAnchors];
|
|
1369
943
|
if (rawAnchors == null || anchorList.length === 0) throw E("path/bad-input", "validate: trustAnchors is required (a single anchor or a non-empty array of anchors)");
|
|
1370
|
-
// With more than one anchor, validate the SAME path against each and accept if ANY yields a valid path
|
|
1371
|
-
// (the trust-store "does this chain lead to any of my roots" question). Two anchors can share a subject
|
|
1372
|
-
// DN with different keys (a rotated or cross-signed root), so choosing by name alone would reject a
|
|
1373
|
-
// valid chain when the wrong same-name anchor is tried first: try each, order-independent for validity.
|
|
1374
|
-
// Each recursion carries exactly one anchor, so it runs the single-anchor body below (no further fan-out).
|
|
1375
944
|
if (anchorList.length > 1) {
|
|
1376
|
-
// Normalize the COMPLETE array up front: toAnchor validates each tuple exactly as the single-anchor
|
|
1377
|
-
// path does, so a malformed anchor is refused (path/bad-input) regardless of its position, never
|
|
1378
|
-
// silently accepted because a valid anchor happened to be tried first.
|
|
1379
945
|
var tuples = [];
|
|
1380
946
|
for (var ti = 0; ti < anchorList.length; ti++) { tuples[ti] = toAnchor(anchorList[ti], "validate"); }
|
|
1381
|
-
// One anchor per recursion, so each runs the single-anchor body below (no further fan-out).
|
|
1382
947
|
var singleOpts = function (a) {
|
|
1383
948
|
var keys = intrinsic.ownKeys(opts);
|
|
1384
949
|
var o = {};
|
|
@@ -1386,10 +951,6 @@ async function validate(path, opts) {
|
|
|
1386
951
|
o.trustAnchors = a;
|
|
1387
952
|
return o;
|
|
1388
953
|
};
|
|
1389
|
-
// Only an anchor whose subject issued certs[0] can complete this path, so try each such candidate and
|
|
1390
|
-
// skip unrelated roots: the cost does not scale with a whole trust store. Same-subject anchors with
|
|
1391
|
-
// different keys (a rotated or cross-signed root) ALL match the terminus and are each tried, so validity
|
|
1392
|
-
// is order-independent.
|
|
1393
954
|
var lastMatched = null, anyMatched = false;
|
|
1394
955
|
for (var ai = 0; ai < tuples.length; ai++) {
|
|
1395
956
|
if (!dnEqual(tuples[ai].name.rdns, certs[0].issuer.rdns)) continue;
|
|
@@ -1398,28 +959,15 @@ async function validate(path, opts) {
|
|
|
1398
959
|
if (oneRes && oneRes.valid) return oneRes;
|
|
1399
960
|
lastMatched = oneRes;
|
|
1400
961
|
}
|
|
1401
|
-
// No supplied anchor's subject matched the path terminus: the chain leads to none of them. Run one to
|
|
1402
|
-
// produce the fail-closed valid:false verdict (the name-chaining check reports it) rather than null.
|
|
1403
962
|
return anyMatched ? lastMatched : validate(path, singleOpts(tuples[0]));
|
|
1404
963
|
}
|
|
1405
964
|
var anchor = toAnchor(anchorList[0], "validate");
|
|
1406
|
-
// The validity-window check is always on (6.1.3(a)(2)); a missing/invalid
|
|
1407
|
-
// check date must fail closed, never silently disable it.
|
|
1408
965
|
guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
|
|
1409
|
-
// Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
|
|
1410
|
-
// throws here rather than silently disabling the behavior it configures.
|
|
1411
|
-
// Validate-if-present (the default is applied where the policy state is built);
|
|
1412
|
-
// the shared integer cap rejects a fractional / negative / non-numeric budget.
|
|
1413
966
|
guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
|
|
1414
|
-
// user-initial-policy-set is a non-empty SET of policy OID strings (6.1.1(c)).
|
|
1415
|
-
// Membership is tested with indexOf, so a raw string here would be consulted
|
|
1416
|
-
// as a SUBSTRING match -- validated to an array of strings instead.
|
|
1417
967
|
if (opts.userInitialPolicySet !== undefined) {
|
|
1418
968
|
var uipsOk = intrinsic.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
|
|
1419
969
|
guard.list.allMatch(opts.userInitialPolicySet, function (p) { return typeof p === "string" && p.length > 0; });
|
|
1420
970
|
if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
|
|
1421
|
-
// Each entry is compared (indexOf) against canonical decoder output, so a
|
|
1422
|
-
// non-canonical key would silently never match -- fail the typo closed here.
|
|
1423
971
|
opts.userInitialPolicySet.forEach(function (p) {
|
|
1424
972
|
guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.userInitialPolicySet entry " + JSON.stringify(p));
|
|
1425
973
|
});
|
|
@@ -1428,9 +976,6 @@ async function validate(path, opts) {
|
|
|
1428
976
|
permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
|
|
1429
977
|
excluded: checkedSubtreeSeeds(opts.initialExcludedSubtrees, "initialExcludedSubtrees"),
|
|
1430
978
|
};
|
|
1431
|
-
// opts.requiredEku -- the key purposes the TARGET certificate must be good
|
|
1432
|
-
// for, each a registered OID name or a dotted OID string. Resolved (and
|
|
1433
|
-
// typo-checked) here at the entry point.
|
|
1434
979
|
var purposeOpts = resolvePurposeOpts(opts);
|
|
1435
980
|
var requiredEku = purposeOpts.requiredEku;
|
|
1436
981
|
var checkPurpose = purposeOpts.checkPurpose;
|
|
@@ -1441,25 +986,10 @@ async function validate(path, opts) {
|
|
|
1441
986
|
var verifier = opts.verifier || null;
|
|
1442
987
|
var revocationChecker = opts.revocationChecker || null;
|
|
1443
988
|
var softFail = opts.softFail === true;
|
|
1444
|
-
// Revocation is a pluggable, opt-in step: by default a path with no checker is
|
|
1445
|
-
// not revocation-checked. opts.requireRevocation makes the 6.1.3(a)(3)
|
|
1446
|
-
// determination mandatory -- an absent checker (or an undetermined result)
|
|
1447
|
-
// then fails the path closed instead of silently skipping the step.
|
|
1448
989
|
var requireRevocation = opts.requireRevocation === true;
|
|
1449
990
|
var failed = false;
|
|
1450
|
-
// Whether the revocation rule RAN, and whether any certificate's answer had to be waived. The
|
|
1451
|
-
// verdict reports the two separately because they are different claims: a caller re-reading a
|
|
1452
|
-
// stored result needs to tell "every certificate was determined not revoked" from "the step was
|
|
1453
|
-
// skipped" from "the step could not conclude and you had asked for that to pass".
|
|
1454
991
|
var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
|
|
1455
|
-
// Which of the anchor's purpose-scoped constraints actually decided anything, reported so a
|
|
1456
|
-
// verdict can be re-read to tell an anchor that was judged from one that carried nothing to judge.
|
|
1457
992
|
var anchorDistrustApplied = false, anchorPurposeApplied = false;
|
|
1458
|
-
// A trust anchor carrying purpose-scoped metadata, validated with no purpose to select by, is a
|
|
1459
|
-
// configuration fault. The constraint is KEYED by purpose -- there is no way to apply
|
|
1460
|
-
// `distrustAfter.serverAuth` without being told the validation is about serverAuth -- so without
|
|
1461
|
-
// one the caller's stated intent would be discarded silently, and a root distrusted years ago
|
|
1462
|
-
// would validate a current leaf. An anchor carrying no such metadata is unaffected.
|
|
1463
993
|
if (!checkPurpose && _hasPurposeScopedMetadata(anchor)) {
|
|
1464
994
|
throw E("path/bad-input", "validate: the trust anchor carries purpose-scoped metadata (distrustAfter / purposes), which is keyed by key purpose -- supply opts.checkPurpose to say which purpose this validation is for, or the constraint cannot be applied");
|
|
1465
995
|
}
|
|
@@ -1469,7 +999,6 @@ async function validate(path, opts) {
|
|
|
1469
999
|
var cert = certs[idx];
|
|
1470
1000
|
var checks = [];
|
|
1471
1001
|
|
|
1472
|
-
// 6.1.3(a)(1) signature.
|
|
1473
1002
|
var sigRes;
|
|
1474
1003
|
if (verifier) {
|
|
1475
1004
|
var vv;
|
|
@@ -1488,29 +1017,18 @@ async function validate(path, opts) {
|
|
|
1488
1017
|
checks.push({ name: "signature", ok: sigRes.ok, code: sigRes.ok ? undefined : (sigRes.code || "path/bad-signature") });
|
|
1489
1018
|
if (!sigRes.ok) failed = true;
|
|
1490
1019
|
|
|
1491
|
-
// draft-ietf-lamps-pq-composite-sigs sec. 5.2: a composite-keyed certificate's
|
|
1492
|
-
// keyUsage must be signature-only (no dual-usage). Runs for the target AND every
|
|
1493
|
-
// intermediate whose own subject key is composite.
|
|
1494
1020
|
if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1495
1021
|
var cku = compositeKeyUsageCheck(cert);
|
|
1496
1022
|
checks.push({ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
|
|
1497
1023
|
if (!cku.ok) failed = true;
|
|
1498
1024
|
}
|
|
1499
1025
|
|
|
1500
|
-
// RFC 9935 sec. 5: an ML-KEM-keyed certificate's keyUsage must be keyEncipherment-only.
|
|
1501
|
-
// Runs for the target AND every intermediate whose own subject key is ML-KEM.
|
|
1502
1026
|
if (ML_KEM_OIDS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1503
1027
|
var kku = kemKeyUsageCheck(cert);
|
|
1504
1028
|
checks.push({ name: "kemKeyUsage", ok: kku.ok, code: kku.ok ? undefined : kku.code });
|
|
1505
1029
|
if (!kku.ok) failed = true;
|
|
1506
1030
|
}
|
|
1507
1031
|
|
|
1508
|
-
// 6.1.3(a)(2) validity window.
|
|
1509
|
-
//
|
|
1510
|
-
// Instants, never the Date objects. Comparing two Dates coerces each one, and coercion runs
|
|
1511
|
-
// `Symbol.toPrimitive` or `valueOf`, which a caller's subclass answers. opts.time was checked
|
|
1512
|
-
// for a valid held instant at entry, and comparing the object here asked it again through a
|
|
1513
|
-
// door the check never used -- so the value validated and the value compared were two reads.
|
|
1514
1032
|
var t = guard.time.instantOf(opts.time);
|
|
1515
1033
|
var vOk = true, vCode;
|
|
1516
1034
|
if (t < guard.time.instantOf(cert.validity.notBefore)) { vOk = false; vCode = "path/not-yet-valid"; }
|
|
@@ -1518,15 +1036,12 @@ async function validate(path, opts) {
|
|
|
1518
1036
|
checks.push({ name: "validity", ok: vOk, code: vCode });
|
|
1519
1037
|
if (!vOk) failed = true;
|
|
1520
1038
|
|
|
1521
|
-
// 6.1.3(a)(4) name chaining.
|
|
1522
1039
|
var chainOk;
|
|
1523
1040
|
try { chainOk = dnEqual(cert.issuer.rdns, state.workingIssuerName.rdns); }
|
|
1524
1041
|
catch (_e) { chainOk = false; }
|
|
1525
1042
|
checks.push({ name: "nameChaining", ok: chainOk === true, code: chainOk === true ? undefined : "path/name-chaining" });
|
|
1526
1043
|
if (chainOk !== true) failed = true;
|
|
1527
1044
|
|
|
1528
|
-
// 6.1.3(b,c) name constraints on this cert's own names (skip for a
|
|
1529
|
-
// self-issued non-terminal cert).
|
|
1530
1045
|
if (!(selfIssued(cert) && i !== n)) {
|
|
1531
1046
|
var ncRes;
|
|
1532
1047
|
try { ncRes = checkNameConstraints(state, cert); }
|
|
@@ -1535,43 +1050,20 @@ async function validate(path, opts) {
|
|
|
1535
1050
|
if (!ncRes.ok) failed = true;
|
|
1536
1051
|
}
|
|
1537
1052
|
|
|
1538
|
-
// 6.1.3(a)(3) revocation.
|
|
1539
1053
|
if (revocationChecker) {
|
|
1540
|
-
var issuerCert = idx > 0 ? certs[idx - 1] : null;
|
|
1054
|
+
var issuerCert = idx > 0 ? certs[idx - 1] : null;
|
|
1541
1055
|
var rv, rvError = null;
|
|
1542
|
-
// A checker that THROWS -- or whose promise rejects -- is not a checker reporting "unknown".
|
|
1543
|
-
// Laundering the two together made a broken checker indistinguishable from a working one that
|
|
1544
|
-
// could not reach the responder, and under softFail both became a pass. The fault is carried
|
|
1545
|
-
// onto the check so an operator can tell their own bug from a network condition, and it fails
|
|
1546
|
-
// the path whatever softFail says (see the branch below).
|
|
1547
1056
|
try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true }); }
|
|
1548
1057
|
catch (e) { rv = { status: "error" }; rvError = e; }
|
|
1549
|
-
// ONLY an explicit "good" is a determined non-revocation; "revoked" fails;
|
|
1550
|
-
// every other value ("unknown", an OCSP tryLater/unauthorized, a typo, a
|
|
1551
|
-
// missing status) is undetermined and fails closed unless softFail.
|
|
1552
|
-
//
|
|
1553
|
-
// The two `ok: true` outcomes are NOT the same claim and no longer the same object. "checked,
|
|
1554
|
-
// and it said good" and "could not check, and you waived it" read identically from a bare
|
|
1555
|
-
// boolean, which is the whole reason a stored verdict cannot be re-read to answer whether
|
|
1556
|
-
// revocation was ever established. Each entry now names the status it was decided on, and a
|
|
1557
|
-
// waiver marks itself.
|
|
1558
1058
|
var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
|
|
1559
1059
|
if (rv && rv.status === "good") { checks.push({ name: "revocation", ok: true, status: "good" }); }
|
|
1560
1060
|
else if (rv && rv.status === "revoked") { checks.push({ name: "revocation", ok: false, status: "revoked", code: "path/revoked" }); failed = true; }
|
|
1561
1061
|
else if (rvError) {
|
|
1562
|
-
// A checker that threw is a FAULT in the checker, not a status it could not reach, and
|
|
1563
|
-
// softFail is the caller opting into an undetermined ANSWER. The built-in CRL and OCSP
|
|
1564
|
-
// checkers return `{status:"unknown"}` for every unreachable or unverifiable condition and
|
|
1565
|
-
// never throw, so a throw here is the caller's own bug -- waiving it would pass the
|
|
1566
|
-
// certificate with no revocation result at all, which is the outcome softFail is asked for
|
|
1567
|
-
// and this is not.
|
|
1568
1062
|
checks.push({ name: "revocation", ok: false, status: "error", code: "path/revocation-checker-error", error: rvError });
|
|
1569
1063
|
revocationUndetermined = true;
|
|
1570
1064
|
failed = true;
|
|
1571
1065
|
}
|
|
1572
1066
|
else if (softFail) {
|
|
1573
|
-
// No `error` slot here or below: a fault took the branch above, so anything reaching these
|
|
1574
|
-
// two is a status the checker actually reported.
|
|
1575
1067
|
checks.push({ name: "revocation", ok: true, status: rvStatus, waived: true });
|
|
1576
1068
|
revocationWaived = true;
|
|
1577
1069
|
} else {
|
|
@@ -1581,53 +1073,36 @@ async function validate(path, opts) {
|
|
|
1581
1073
|
}
|
|
1582
1074
|
revocationRan = true;
|
|
1583
1075
|
} else if (requireRevocation) {
|
|
1584
|
-
// No checker was supplied but the caller demands a revocation determination:
|
|
1585
|
-
// the step cannot be performed, so fail closed (never silently skip).
|
|
1586
1076
|
checks.push({ name: "revocation", ok: false, code: "path/revocation-undetermined" }); failed = true;
|
|
1587
1077
|
}
|
|
1588
1078
|
|
|
1589
|
-
// 6.1.3(d-f) policies.
|
|
1590
1079
|
var polRes = processPolicies(state, cert, i, checks);
|
|
1591
1080
|
if (state._capHit) { checks.push({ name: "policyTree", ok: false, code: "path/policy-tree-cap" }); failed = true; }
|
|
1592
1081
|
else if (polRes.fatal) failed = true;
|
|
1593
1082
|
|
|
1594
|
-
// empty subject requires a critical SAN (4.1.2.6).
|
|
1595
1083
|
if (cert.subject.rdns.length === 0) {
|
|
1596
1084
|
var san = findExt(cert, OID.subjectAltName);
|
|
1597
1085
|
if (!san || !san.critical) { checks.push({ name: "emptySubject", ok: false, code: "path/empty-subject-no-critical-san" }); failed = true; }
|
|
1598
1086
|
}
|
|
1599
1087
|
|
|
1600
|
-
// 6.1.4 / 6.1.5.
|
|
1601
1088
|
if (i !== n) {
|
|
1602
1089
|
if (!state._capHit) {
|
|
1603
1090
|
var prep = prepareNext(state, cert, i, checks);
|
|
1604
1091
|
if (prep.fatal) failed = true;
|
|
1605
1092
|
}
|
|
1606
1093
|
} else {
|
|
1607
|
-
|
|
1608
|
-
if (state.explicitPolicy > 0) state.explicitPolicy--; // 6.1.5(a)
|
|
1094
|
+
if (state.explicitPolicy > 0) state.explicitPolicy--;
|
|
1609
1095
|
var lpc;
|
|
1610
1096
|
try { lpc = decodeExt(cert, OID.policyConstraints); }
|
|
1611
1097
|
catch (_e) { lpc = null; checks.push({ name: "policyConstraints", ok: false, code: "path/bad-policy" }); failed = true; }
|
|
1612
|
-
// 4.2.1.11: policyConstraints MUST be critical -- apply the same check the
|
|
1613
|
-
// intermediate path (prepareNext) uses, so a non-critical policyConstraints
|
|
1614
|
-
// on the TARGET cert fails closed consistently.
|
|
1615
1098
|
if (requireCriticalExt(lpc, "policyConstraints", checks)) failed = true;
|
|
1616
|
-
if (lpc && lpc.value.requireExplicitPolicy === 0) state.explicitPolicy = 0;
|
|
1617
|
-
// 6.1.4(a) / 4.2.1.5: policyMappings must never map to/from anyPolicy. The
|
|
1618
|
-
// intermediate path enforces this in prepareNext; the target cert skips it,
|
|
1619
|
-
// so apply the structural rejection here too (covers a non-critical mapping
|
|
1620
|
-
// that the unrecognized-critical check above does not reach).
|
|
1099
|
+
if (lpc && lpc.value.requireExplicitPolicy === 0) state.explicitPolicy = 0;
|
|
1621
1100
|
var lpm;
|
|
1622
1101
|
try { lpm = decodeExt(cert, OID.policyMappings); }
|
|
1623
1102
|
catch (_e) { lpm = null; checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true; }
|
|
1624
1103
|
if (lpm && mapsAnyPolicy(lpm.value)) {
|
|
1625
1104
|
checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
|
|
1626
1105
|
}
|
|
1627
|
-
// 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
|
|
1628
|
-
// critical wherever they appear -- apply to the TARGET cert the same
|
|
1629
|
-
// check prepareNext applies to every intermediate (their semantic gates
|
|
1630
|
-
// do not run for the target, but the criticality rule still binds).
|
|
1631
1106
|
var lnc;
|
|
1632
1107
|
try { lnc = decodeExt(cert, OID.nameConstraints); }
|
|
1633
1108
|
catch (e) { lnc = null; checks.push({ name: "nameConstraints", ok: false, code: pathCode(e, "path/bad-name-constraints") }); failed = true; }
|
|
@@ -1636,64 +1111,33 @@ async function validate(path, opts) {
|
|
|
1636
1111
|
try { liap = decodeExt(cert, OID.inhibitAnyPolicy); }
|
|
1637
1112
|
catch (e) { liap = null; checks.push({ name: "inhibitAnyPolicy", ok: false, code: pathCode(e, "path/bad-policy") }); failed = true; }
|
|
1638
1113
|
if (requireCriticalExt(liap, "inhibitAnyPolicy", checks)) failed = true;
|
|
1639
|
-
// 4.2.1.12: when the caller states required key purposes, the target's
|
|
1640
|
-
// extendedKeyUsage must assert every one (or anyExtendedKeyUsage -- the
|
|
1641
|
-
// 4.2.1.12 wildcard; rejecting it is an application MAY, not the
|
|
1642
|
-
// default). An ABSENT extension leaves the key unrestricted, so it
|
|
1643
|
-
// satisfies any required purpose.
|
|
1644
1114
|
if (requiredEku && ekuPurposeFails(cert, requiredEku, checks)) failed = true;
|
|
1645
|
-
// Trust-anchor constraint contract (NSS / CCADB metadata; gated so a bare
|
|
1646
|
-
// anchor or an absent checkPurpose is byte-identical to today). The anchor's
|
|
1647
|
-
// per-purpose distrust-after date and delegator purposes apply to the
|
|
1648
|
-
// end-entity leaf it ultimately certifies.
|
|
1649
1115
|
var ta = anchor;
|
|
1650
|
-
// A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a
|
|
1651
|
-
// NaN time) would make `notBefore > it` NaN-false and SILENTLY drop the distrust
|
|
1652
|
-
// restriction -- the NaN-Date fail-open. Validate a present date fail-closed
|
|
1653
|
-
// before the comparison; an absent (undefined/null) date is no restriction.
|
|
1654
1116
|
var distrustDate = assertAnchorConstraints(ta, checkPurpose);
|
|
1655
1117
|
if (distrustDate != null) {
|
|
1656
1118
|
anchorDistrustApplied = true;
|
|
1657
|
-
// STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
|
|
1658
|
-
// (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
|
|
1659
|
-
// <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
|
|
1660
|
-
// convention keeps the whole boundary day trusted).
|
|
1661
1119
|
if (guard.time.instantOf(cert.validity.notBefore) > guard.time.instantOf(distrustDate)) {
|
|
1662
1120
|
checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
|
|
1663
1121
|
}
|
|
1664
1122
|
}
|
|
1665
1123
|
if (checkPurpose && ta.purposes) {
|
|
1666
1124
|
anchorPurposeApplied = true;
|
|
1667
|
-
// The map is the operator's RESTRICTION, so it is asked for its own entry: an inherited
|
|
1668
|
-
// `true` would grant the purpose on every anchor whose map does not name it.
|
|
1669
1125
|
if (!intrinsic.hasOwn(ta.purposes, checkPurpose) || ta.purposes[checkPurpose] !== true) {
|
|
1670
1126
|
checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
|
|
1671
1127
|
}
|
|
1672
1128
|
}
|
|
1673
|
-
updateWorkingKey(state, cert);
|
|
1129
|
+
updateWorkingKey(state, cert);
|
|
1674
1130
|
}
|
|
1675
1131
|
|
|
1676
|
-
// 6.1.4(o) / 6.1.5(e) unrecognized critical extension.
|
|
1677
1132
|
var unk = unrecognizedCriticalExtension(cert, i === n);
|
|
1678
1133
|
if (unk) { checks.push({ name: "criticalExtensions", ok: false, code: "path/unrecognized-critical-extension" }); failed = true; }
|
|
1679
1134
|
|
|
1680
|
-
// A RECOGNIZED critical extension must still be structurally valid even
|
|
1681
|
-
// when its semantic gate does not run on this cert (the leaf is not subject
|
|
1682
|
-
// to 6.1.4, so its basicConstraints/keyUsage/policy* are never read in
|
|
1683
|
-
// prepareNext) -- a malformed critical extnValue must fail closed, not slip
|
|
1684
|
-
// through as "recognized". Decode every recognized critical extension to
|
|
1685
|
-
// validate it (a no-op for one already decoded above; the decoders are pure).
|
|
1686
1135
|
var crit = validateCriticalExtensionStructure(cert);
|
|
1687
1136
|
if (crit) { checks.push({ name: "criticalExtensionValue", ok: false, code: crit }); failed = true; }
|
|
1688
1137
|
|
|
1689
1138
|
state.results.push({ index: idx, checks: checks });
|
|
1690
1139
|
}
|
|
1691
1140
|
|
|
1692
|
-
// 6.1.5(g) success condition. The tree is first INTERSECTED with the
|
|
1693
|
-
// user-initial-policy-set (userConstrainedPolicies); success requires
|
|
1694
|
-
// explicit_policy > 0 OR that pruned tree to be non-empty. Using the raw tree
|
|
1695
|
-
// would accept a path whose only surviving policies are outside the user set
|
|
1696
|
-
// when an explicit policy is required.
|
|
1697
1141
|
var ucps = userConstrainedPolicies(state, n);
|
|
1698
1142
|
var policyOk = state.explicitPolicy > 0 || ucps.length > 0;
|
|
1699
1143
|
if (!policyOk) {
|
|
@@ -1704,7 +1148,6 @@ async function validate(path, opts) {
|
|
|
1704
1148
|
failed = true;
|
|
1705
1149
|
}
|
|
1706
1150
|
|
|
1707
|
-
// Opt-in CT gate: verify the target's embedded SCTs and record the verdict on its checks (RFC 6962).
|
|
1708
1151
|
if (opts.ctLogList != null) {
|
|
1709
1152
|
var ctCheck = await ctGateCheck(certs[n - 1], certs, anchor, opts.ctLogList, opts.ctPolicy || {}, n, opts.time);
|
|
1710
1153
|
var target = null;
|
|
@@ -1715,12 +1158,6 @@ async function validate(path, opts) {
|
|
|
1715
1158
|
|
|
1716
1159
|
return {
|
|
1717
1160
|
valid: !failed,
|
|
1718
|
-
// What the revocation rule and the anchor's own trust metadata actually decided. `valid` alone
|
|
1719
|
-
// cannot answer either, and both are questions a stored verdict is re-read to settle: was this
|
|
1720
|
-
// certificate ever established as un-revoked, and was the anchor's distrust date consulted.
|
|
1721
|
-
// The WEAKEST outcome on the path, not the fact that a checker ran: a certificate nobody could
|
|
1722
|
-
// answer for leaves revocation unestablished however many others answered, and deriving the word
|
|
1723
|
-
// from "a checker ran" put that run on the same value as one that established every answer.
|
|
1724
1161
|
revocationChecked: !revocationRan ? false
|
|
1725
1162
|
: revocationUndetermined ? "undetermined"
|
|
1726
1163
|
: revocationWaived ? "waived" : "determined",
|
|
@@ -1735,8 +1172,6 @@ async function validate(path, opts) {
|
|
|
1735
1172
|
workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm,
|
|
1736
1173
|
workingPublicKeyParameters: state.workingPublicKeyParameters,
|
|
1737
1174
|
validPolicyTree: treeWithoutParent(state.validPolicyTree),
|
|
1738
|
-
// 6.1.5(f): the authority-constrained policy set = the leaf-depth policies
|
|
1739
|
-
// in the valid-policy tree intersected with user-initial-policy-set.
|
|
1740
1175
|
userConstrainedPolicySet: ucps,
|
|
1741
1176
|
};
|
|
1742
1177
|
}
|
|
@@ -1753,9 +1188,6 @@ function userConstrainedPolicies(state, n) {
|
|
|
1753
1188
|
});
|
|
1754
1189
|
var set = {};
|
|
1755
1190
|
Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
|
|
1756
|
-
// 6.1.5(g) step 3: a depth-n anyPolicy node under a restrictive user set
|
|
1757
|
-
// expands to each user policy (the intersection of anyPolicy with the user
|
|
1758
|
-
// set is the user set itself).
|
|
1759
1191
|
if (hasAnyLeaf) {
|
|
1760
1192
|
if (anyUser) set[OID.anyPolicy] = true;
|
|
1761
1193
|
else uips.forEach(function (p) { set[p] = true; });
|
|
@@ -1763,7 +1195,6 @@ function userConstrainedPolicies(state, n) {
|
|
|
1763
1195
|
return Object.keys(set);
|
|
1764
1196
|
}
|
|
1765
1197
|
|
|
1766
|
-
// ---- the CRL revocation checker (6.3) -------------------------------------
|
|
1767
1198
|
|
|
1768
1199
|
var OID_IDP = oid.byName("issuingDistributionPoint");
|
|
1769
1200
|
var OID_DELTA_CRL = oid.byName("deltaCRLIndicator");
|
|
@@ -1771,41 +1202,15 @@ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
|
|
|
1771
1202
|
var OID_CRL_NUMBER = oid.byName("cRLNumber");
|
|
1772
1203
|
var OID_FRESHEST_CRL = oid.byName("freshestCRL");
|
|
1773
1204
|
|
|
1774
|
-
// The RFC 5280 sec. 5.2.5 IssuingDistributionPoint grammar, shared with the CRL
|
|
1775
|
-
// verbs (pkix.issuingDistributionPoint) so the scope this validator reads and the
|
|
1776
|
-
// scope pki.crl.isRevoked refuses to answer past are read by the same rules.
|
|
1777
1205
|
var IDP_SCHEMA = pkix.issuingDistributionPoint("path/bad-idp");
|
|
1778
1206
|
|
|
1779
|
-
// RFC 5280 sec. 6.3.2(a): the legal members of reasons_mask are exactly the eight
|
|
1780
|
-
// named ReasonFlags bits, 1..8 (`unused` bit 0 is not a reason, and `unspecified`
|
|
1781
|
-
// has no ReasonFlags bit at all). 0x1FE is bits 1..8 set.
|
|
1782
|
-
//
|
|
1783
|
-
// NOTE the numbering divergence, which is the trap in this area: a ReasonFlags BIT
|
|
1784
|
-
// is not a CRLReason VALUE. Bits 1..6 coincide with codes 1..6, but bit 7 is
|
|
1785
|
-
// privilegeWithdrawn (code 9) and bit 8 is aACompromise (code 10). sec. 6.3.3 never
|
|
1786
|
-
// needs a mapping between them -- the mask comes only from IDP/DP ReasonFlags and
|
|
1787
|
-
// cert_status only from an entry's CRLReason -- so no bit<->code table exists here,
|
|
1788
|
-
// deliberately. constants.NAMES.REASON_FLAGS and NAMES.CRL_REASON stay separate.
|
|
1789
1207
|
var ALL_REASONS = 0x1FE;
|
|
1790
1208
|
|
|
1791
|
-
// A ReasonFlags BIT STRING { unusedBits, bytes } -> a bit mask, or null when the
|
|
1792
|
-
// encoding is not minimal DER (X.690 sec. 11.2.2 NamedBitList: trailing zero bits
|
|
1793
|
-
// MUST be dropped). A null tells the caller the CRL's scope is unknown, which is
|
|
1794
|
-
// the same fail-closed posture decodeIdp already takes for a malformed IDP.
|
|
1795
|
-
//
|
|
1796
|
-
// Bits at or above 9 are undefined by sec. 4.2.1.13. They are IGNORED rather than
|
|
1797
|
-
// rejected: an undefined bit can never help reach all-reasons, so ignoring it can
|
|
1798
|
-
// only withhold coverage, never grant it. Reading a bounded prefix also means a
|
|
1799
|
-
// hostile multi-kilobyte ReasonFlags costs nothing.
|
|
1800
1209
|
function reasonMaskFromBitString(bs) {
|
|
1801
1210
|
if (!bs || !bs.bytes) return null;
|
|
1802
1211
|
try { schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (msg) { throw E("path/bad-idp", msg); }); }
|
|
1803
1212
|
catch (_e) { return null; }
|
|
1804
1213
|
var mask = 0;
|
|
1805
|
-
// From bit 1: sec. 6.3.2(a) defines the legal members as the eight NAMED reasons, and ReasonFlags
|
|
1806
|
-
// bit 0 is -- not a reason at all. Admitting it could not grant coverage (the completeness
|
|
1807
|
-
// test masks with ALL_REASONS) but would make a shard that asserts ONLY bit 0 look like partial
|
|
1808
|
-
// coverage rather than none, which is the wrong verdict to report.
|
|
1809
1214
|
for (var bit = 1; bit <= 8; bit++) {
|
|
1810
1215
|
var byteI = bit >> 3;
|
|
1811
1216
|
if (byteI >= bs.bytes.length) break;
|
|
@@ -1814,34 +1219,19 @@ function reasonMaskFromBitString(bs) {
|
|
|
1814
1219
|
return mask;
|
|
1815
1220
|
}
|
|
1816
1221
|
|
|
1817
|
-
// RFC 5280 sec. 6.3.3(d)(1)-(4) -- the interim_reasons_mask, in ONE place.
|
|
1818
|
-
// (1) both present -> intersection
|
|
1819
|
-
// (2) IDP present, DP absent -> the IDP's reasons
|
|
1820
|
-
// (3) IDP absent, DP present -> the DP's reasons
|
|
1821
|
-
// (4) both absent -> all-reasons
|
|
1822
|
-
// A null mask means "present but unreadable"; the caller has already marked such a
|
|
1823
|
-
// CRL unusable, so treating it as 0 here is belt-and-braces, never a coverage grant.
|
|
1824
1222
|
function interimReasonMask(idpMask, dpMask) {
|
|
1825
|
-
if (idpMask != null && dpMask != null) return idpMask & dpMask;
|
|
1826
|
-
if (idpMask != null) return idpMask;
|
|
1827
|
-
if (dpMask != null) return dpMask;
|
|
1828
|
-
return ALL_REASONS;
|
|
1223
|
+
if (idpMask != null && dpMask != null) return idpMask & dpMask;
|
|
1224
|
+
if (idpMask != null) return idpMask;
|
|
1225
|
+
if (dpMask != null) return dpMask;
|
|
1226
|
+
return ALL_REASONS;
|
|
1829
1227
|
}
|
|
1830
1228
|
|
|
1831
1229
|
function decodeIdp(ext) {
|
|
1832
|
-
// Surface the scope flags the checker gates on. ANY structural or value fault
|
|
1833
|
-
// -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
|
|
1834
|
-
// an encoded-FALSE default, a malformed DistributionPointName -- leaves the
|
|
1835
|
-
// CRL's scope unknown: the CRL is unusable, never assumed unrestricted.
|
|
1836
1230
|
var out = { hasDistributionPoint: false, distributionPoint: null, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
|
|
1837
1231
|
var m;
|
|
1838
1232
|
try {
|
|
1839
1233
|
m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS);
|
|
1840
1234
|
if (m.fields.distributionPoint.present) {
|
|
1841
|
-
// distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE
|
|
1842
|
-
// (a context tag on a CHOICE-typed field is always EXPLICIT). The decoded
|
|
1843
|
-
// name feeds the sec. 6.3.3(b)(2)(i) correspondence against the
|
|
1844
|
-
// certificate's own DistributionPoints in the checker below.
|
|
1845
1235
|
var dpnWrap = m.fields.distributionPoint.node;
|
|
1846
1236
|
if (!dpnWrap.children || dpnWrap.children.length !== 1) {
|
|
1847
1237
|
throw E("path/bad-idp", "IssuingDistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
|
|
@@ -1852,8 +1242,6 @@ function decodeIdp(ext) {
|
|
|
1852
1242
|
catch (_e) { out.malformed = true; return out; }
|
|
1853
1243
|
function flag(f) {
|
|
1854
1244
|
if (!f.present) return false;
|
|
1855
|
-
// A present flag must encode DER-TRUE; a FALSE is the omitted DEFAULT
|
|
1856
|
-
// (X.690 sec. 11.5), so mark the scope unknown and report not-set.
|
|
1857
1245
|
var isSet = f.value === true;
|
|
1858
1246
|
if (!isSet) out.malformed = true;
|
|
1859
1247
|
return isSet;
|
|
@@ -1862,10 +1250,6 @@ function decodeIdp(ext) {
|
|
|
1862
1250
|
out.onlyUser = flag(m.fields.onlyContainsUserCerts);
|
|
1863
1251
|
out.onlyCa = flag(m.fields.onlyContainsCACerts);
|
|
1864
1252
|
out.onlySomeReasons = m.fields.onlySomeReasons.present ? true : null;
|
|
1865
|
-
// The reason BITS, not just their presence: sec. 6.3.3(d)(1)/(d)(2) need the
|
|
1866
|
-
// value. A present-but-non-minimal ReasonFlags leaves the CRL's scope unknown,
|
|
1867
|
-
// which is the same unusable posture every other IDP fault takes -- otherwise
|
|
1868
|
-
// the (d)(1) intersection would be computed over an encoding DER forbids.
|
|
1869
1253
|
out.onlySomeReasonsMask = null;
|
|
1870
1254
|
if (m.fields.onlySomeReasons.present) {
|
|
1871
1255
|
out.onlySomeReasonsMask = reasonMaskFromBitString(m.fields.onlySomeReasons.value);
|
|
@@ -1876,17 +1260,6 @@ function decodeIdp(ext) {
|
|
|
1876
1260
|
return out;
|
|
1877
1261
|
}
|
|
1878
1262
|
|
|
1879
|
-
// RFC 5280 sec. 6.3.3(b)(2)(i): find the certificate DistributionPoint that
|
|
1880
|
-
// CORRESPONDS to a shard CRL's IDP distribution point name. The correspondence
|
|
1881
|
-
// rule itself -- at least one name in common, compared by BYTE-IDENTICAL DER,
|
|
1882
|
-
// mixed name forms never corresponding -- is guard.name.dpnCorresponds, which
|
|
1883
|
-
// is also what a CMP crlUpdate answer is held to, so one definition decides
|
|
1884
|
-
// whether a CRL speaks for a point wherever the question is asked. Per sec.
|
|
1885
|
-
// 6.3.3(b)(1), a DP naming a cRLIssuer participates only when that cRLIssuer
|
|
1886
|
-
// is the certificate issuer itself: the checker only consults CRLs issued BY
|
|
1887
|
-
// the certificate issuer and rejects indirect CRLs, so a DP delegated to
|
|
1888
|
-
// another CRL issuer is out of play. Returns the matched DP (its `reasons`
|
|
1889
|
-
// feeds the coarse reason-mask rule) or null.
|
|
1890
1263
|
function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
|
|
1891
1264
|
if (!idpDpn || !certDPs) return null;
|
|
1892
1265
|
for (var i = 0; i < certDPs.length; i++) {
|
|
@@ -1898,28 +1271,16 @@ function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
|
|
|
1898
1271
|
return null;
|
|
1899
1272
|
}
|
|
1900
1273
|
|
|
1901
|
-
// Does a DistributionPoint's cRLIssuer name the certificate issuer? True iff
|
|
1902
|
-
// one of its GeneralNames is a directoryName equal to the issuer DN under the
|
|
1903
|
-
// RFC 5280 sec. 7.1 comparison (the shared name guard, via this file's dnEqual
|
|
1904
|
-
// wrapper). Any fault -- a DN the comparison rejects for an embedded control
|
|
1905
|
-
// byte -- resolves false: the DP stays out of the correspondence (fail closed).
|
|
1906
1274
|
function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
|
|
1907
1275
|
for (var i = 0; i < cRLIssuer.names.length; i++) {
|
|
1908
1276
|
var n = cRLIssuer.names[i];
|
|
1909
1277
|
if (n.tagNumber !== 4 || !n.value || !n.value.rdns) continue;
|
|
1910
|
-
// dnEqual throws only on a control-byte / malformed cRLIssuer DN; returning false excludes
|
|
1911
|
-
// that DP from the correspondence (a malformed indirect-CRL issuer name never corresponds).
|
|
1912
1278
|
try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
|
|
1913
1279
|
catch (_e) { return false; }
|
|
1914
1280
|
}
|
|
1915
1281
|
return false;
|
|
1916
1282
|
}
|
|
1917
1283
|
|
|
1918
|
-
// The raw extnValue octets of a CRL extension, or null. sec. 6.3.3(c)(2)/(c)(3)
|
|
1919
|
-
// compare the IDP and authorityKeyIdentifier of a delta and its base for identity;
|
|
1920
|
-
// the comparison is over the exact encoded octets (the sec. 5.2.5 @3648
|
|
1921
|
-
// "identical encoding" discipline already used for distributionPoint matching),
|
|
1922
|
-
// never over a re-normalized decode.
|
|
1923
1284
|
function crlExtValue(theCrl, wantOid) {
|
|
1924
1285
|
for (var i = 0; i < theCrl.crlExtensions.length; i++) {
|
|
1925
1286
|
if (theCrl.crlExtensions[i].oid === wantOid) return theCrl.crlExtensions[i].value;
|
|
@@ -1927,15 +1288,6 @@ function crlExtValue(theCrl, wantOid) {
|
|
|
1927
1288
|
return null;
|
|
1928
1289
|
}
|
|
1929
1290
|
|
|
1930
|
-
// One pass over the bundle: split complete CRLs from deltas and decode, per CRL,
|
|
1931
|
-
// the values the merge preconditions compare. A delta whose BaseCRLNumber will not
|
|
1932
|
-
// decode as a non-negative INTEGER is unusable AS A DELTA but stays consultable for
|
|
1933
|
-
// revocation -- dropping it wholesale would lose a revocation it lists (T1).
|
|
1934
|
-
// RFC 5280 sec. 5.2.3 @3404 bounds a CRL number at 20 octets, and pki.crl.sign enforces that on
|
|
1935
|
-
// emission. A number past it is non-conforming, so it does not earn the merge -- which can RELEASE
|
|
1936
|
-
// a certificate. The value still decodes and the CRL is still consulted for revocation; only the
|
|
1937
|
-
// new capability is withheld. Measured on the ENCODED length, matching the producer-side check
|
|
1938
|
-
// rather than a re-derived numeric bound.
|
|
1939
1291
|
function crlNumberWithinBound(n) {
|
|
1940
1292
|
if (typeof n !== "bigint" || n < 0n) return false;
|
|
1941
1293
|
try { return asn1.decode(asn1.build.integer(n)).content.length <= 20; }
|
|
@@ -1951,9 +1303,6 @@ function classifyCrls(parsed) {
|
|
|
1951
1303
|
for (var dz = 0; dz < theCrl.crlExtensions.length; dz++) {
|
|
1952
1304
|
if (theCrl.crlExtensions[dz].oid === OID_DELTA_CRL) { deltaCritical = theCrl.crlExtensions[dz].critical === true; break; }
|
|
1953
1305
|
}
|
|
1954
|
-
// schema-crl already decoded cRLNumber to a non-negative BigInt; the IDP,
|
|
1955
|
-
// authorityKeyIdentifier and deltaCRLIndicator values stay RAW octets there,
|
|
1956
|
-
// which is exactly what the byte-identical compares below need.
|
|
1957
1306
|
var num = crlExtValue(theCrl, OID_CRL_NUMBER);
|
|
1958
1307
|
var rec = {
|
|
1959
1308
|
crl: theCrl,
|
|
@@ -1966,47 +1315,24 @@ function classifyCrls(parsed) {
|
|
|
1966
1315
|
if (deltaRaw === null) { completes.push(rec); continue; }
|
|
1967
1316
|
try {
|
|
1968
1317
|
var n = asn1.read.integer(asn1.decode(deltaRaw));
|
|
1969
|
-
// sec. 5.2.3 @3404: a CRL number may be up to 20 octets. It stays a BigInt
|
|
1970
|
-
// through every comparison below -- narrowing through Number would collapse
|
|
1971
|
-
// large values and make 5.2.4(c)/(d) compare the wrong things.
|
|
1972
|
-
// sec. 5.2.4 @3447 makes deltaCRLIndicator a MUST-be-critical extension. A non-critical one
|
|
1973
|
-
// is non-conforming, so it does not earn the NEW capability of being merged -- merging can
|
|
1974
|
-
// RELEASE a certificate, and that must rest on a conforming indicator. It is still classified
|
|
1975
|
-
// as a delta and still consulted for revocation, which is the shipped behavior and the
|
|
1976
|
-
// conservative direction.
|
|
1977
1318
|
if (crlNumberWithinBound(n) && deltaCritical) { rec.baseCrlNumber = n; rec.mergeable = true; }
|
|
1978
1319
|
} catch (_e) {
|
|
1979
|
-
// Unusable AS A DELTA, but still scanned for revocation. That is the conservative direction:
|
|
1980
|
-
// the CRL is still the issuer's, signed and current (it must pass every gate before it is
|
|
1981
|
-
// consulted), so a serial it lists is a genuine revocation and dropping it wholesale could
|
|
1982
|
-
// lose one. Consulting it can only ever find MORE revocations, never grant coverage -- a
|
|
1983
|
-
// delta contributes no reason mask.
|
|
1984
1320
|
}
|
|
1985
1321
|
deltas.push(rec);
|
|
1986
1322
|
}
|
|
1987
1323
|
return { completes: completes, deltas: deltas };
|
|
1988
1324
|
}
|
|
1989
1325
|
|
|
1990
|
-
// RFC 5280 sec. 6.3.3(c)(1)-(3) + sec. 5.2.4(a)-(d), as one predicate. Every
|
|
1991
|
-
// comparison is BigInt or byte-exact: no narrowing, no canonicalization. An
|
|
1992
|
-
// extension ABSENT on both sides matches; present on one side only does not.
|
|
1993
1326
|
function deltaMergesWith(delta, complete) {
|
|
1994
1327
|
if (!delta.mergeable) return false;
|
|
1995
|
-
// Both numbers are required: 5.2.4(c)/(d) are ordering rules, and a missing
|
|
1996
|
-
// cRLNumber on either side leaves the ordering unknowable -- fail closed.
|
|
1997
1328
|
if (complete.crlNumber === null || delta.crlNumber === null) return false;
|
|
1998
|
-
if (!dnEqualUsable(delta.crl.issuer.rdns, complete.crl.issuer.rdns)) return false;
|
|
1999
|
-
if (!sameRawExt(delta.idpRaw, complete.idpRaw)) return false;
|
|
2000
|
-
if (!sameRawExt(delta.akiRaw, complete.akiRaw)) return false;
|
|
2001
|
-
if (!(complete.crlNumber >= delta.baseCrlNumber)) return false;
|
|
2002
|
-
if (!(complete.crlNumber < delta.crlNumber)) return false;
|
|
1329
|
+
if (!dnEqualUsable(delta.crl.issuer.rdns, complete.crl.issuer.rdns)) return false;
|
|
1330
|
+
if (!sameRawExt(delta.idpRaw, complete.idpRaw)) return false;
|
|
1331
|
+
if (!sameRawExt(delta.akiRaw, complete.akiRaw)) return false;
|
|
1332
|
+
if (!(complete.crlNumber >= delta.baseCrlNumber)) return false;
|
|
1333
|
+
if (!(complete.crlNumber < delta.crlNumber)) return false;
|
|
2003
1334
|
return true;
|
|
2004
1335
|
}
|
|
2005
|
-
// dnEqual THROWS on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
|
|
2006
|
-
// For the merge preconditions a DN that cannot be compared is simply not a match:
|
|
2007
|
-
// the delta and the base are not shown to share an issuer, so they are not merged.
|
|
2008
|
-
// The throw is contained here rather than at the call site so the predicate itself
|
|
2009
|
-
// stays a straight-line sequence of comparisons.
|
|
2010
1336
|
function dnEqualUsable(a, b) {
|
|
2011
1337
|
try { return dnEqual(a, b); }
|
|
2012
1338
|
catch (_e) { return false; }
|
|
@@ -2018,10 +1344,6 @@ function sameRawExt(a, b) {
|
|
|
2018
1344
|
return a.equals(b);
|
|
2019
1345
|
}
|
|
2020
1346
|
|
|
2021
|
-
// sec. 5.2.4 @3580 makes MULTIPLE current deltas for one scope legal and says the
|
|
2022
|
-
// application SHOULD take the one with the latest thisUpdate. Selection, never
|
|
2023
|
-
// rejection: treating two current deltas as a fault would refuse a conforming
|
|
2024
|
-
// publication. Ties break on the greater cRLNumber, then first-wins.
|
|
2025
1347
|
function selectDelta(candidates) {
|
|
2026
1348
|
var best = null;
|
|
2027
1349
|
for (var i = 0; i < candidates.length; i++) {
|
|
@@ -2079,31 +1401,14 @@ function selectDelta(candidates) {
|
|
|
2079
1401
|
*/
|
|
2080
1402
|
function crlChecker(crls, opts) {
|
|
2081
1403
|
var parsed = (crls || []).map(function (c, ci) {
|
|
2082
|
-
// Re-derived from the bytes the parser read, exactly as a certificate is. A CRL's answer is a
|
|
2083
|
-
// verdict, and its signature covers a byte range while the revocation list and the scope
|
|
2084
|
-
// extensions are separate properties of the parsed object: keep a correctly signed CRL's
|
|
2085
|
-
// `tbsBytes` and signature, empty `revokedCertificates`, and the signature still verifies while
|
|
2086
|
-
// a revoked certificate reports good. The scope fields fail the same way -- an emptied
|
|
2087
|
-
// `crlExtensions` turns a scope-restricted CRL into one that answers for everything.
|
|
2088
1404
|
return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
|
|
2089
1405
|
"crlChecker: crls[" + ci + "]");
|
|
2090
1406
|
});
|
|
2091
|
-
// RFC 5280 sec. 6.3.1(b): use-deltas is an INPUT to the algorithm. Default ON --
|
|
2092
|
-
// a caller holding a delta wants it used -- and turning it off never makes a
|
|
2093
|
-
// verdict weaker, only less determined.
|
|
2094
1407
|
var useDeltas = !(opts && opts.useDeltas === false);
|
|
2095
1408
|
return {
|
|
2096
1409
|
check: async function (cert, issuer, ctx) {
|
|
2097
1410
|
var time = ctx.time;
|
|
2098
1411
|
var historical = ctx.historicalMode === true;
|
|
2099
|
-
// The cert's CA-ness gates the IDP scope flags (onlyContainsUserCerts /
|
|
2100
|
-
// onlyContainsCACerts, RFC 5280 sec. 6.3.3(b)(2)). An UNREADABLE
|
|
2101
|
-
// basicConstraints leaves that scope undeterminable -- guessing "not a
|
|
2102
|
-
// CA" would consult a user-only CRL for what may be a CA certificate
|
|
2103
|
-
// (out of the CRL's scope, so its silence proves nothing) -- so certIsCa
|
|
2104
|
-
// stays null, BOTH scoped forms are skipped below, and only a
|
|
2105
|
-
// full-scope CRL can speak for this certificate. The decode fault is
|
|
2106
|
-
// carried into the undetermined verdict's reason.
|
|
2107
1412
|
var certIsCa = null, certScopeFault = null;
|
|
2108
1413
|
try {
|
|
2109
1414
|
var bc = decodeExt(cert, OID.basicConstraints);
|
|
@@ -2111,26 +1416,12 @@ function crlChecker(crls, opts) {
|
|
|
2111
1416
|
} catch (e) {
|
|
2112
1417
|
certScopeFault = pathCode(e, "path/bad-extension-value");
|
|
2113
1418
|
}
|
|
2114
|
-
// The certificate's own cRLDistributionPoints, decoded once per check:
|
|
2115
|
-
// the sec. 6.3.3(b)(2)(i) correspondence gate below needs the DP names.
|
|
2116
|
-
// A decode fault leaves certDPs null -- every DP-scoped shard then stays
|
|
2117
|
-
// revocation-only (no correspondence can be shown; fail closed) while
|
|
2118
|
-
// the full-CRL path is unaffected (mirrors the basicConstraints
|
|
2119
|
-
// handling above); the check never crashes on a malformed extension.
|
|
2120
1419
|
var certDPs = null;
|
|
2121
1420
|
var certCdpExt = findExt(cert, OID.cRLDistributionPoints);
|
|
2122
1421
|
if (certCdpExt) {
|
|
2123
1422
|
try { certDPs = EXT_DECODERS[OID.cRLDistributionPoints](certCdpExt.value); }
|
|
2124
1423
|
catch (_e) { certDPs = null; }
|
|
2125
1424
|
}
|
|
2126
|
-
// RFC 5280 sec. 6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
|
|
2127
|
-
// certificate, the cRLSign bit must be VERIFIED set. An issuer that OMITS
|
|
2128
|
-
// keyUsage is unconstrained (the same rule the sec. 6.1.4(n) keyCertSign gate
|
|
2129
|
-
// applies to certificate signing), so its CRL is authoritative. The anchor
|
|
2130
|
-
// is likewise unconstrained (issuerCert is null for the cert it directly
|
|
2131
|
-
// issued). A PRESENT-but-unreadable keyUsage cannot be verified -- treating
|
|
2132
|
-
// it like an absent one would let garbage keyUsage bytes authorize CRL
|
|
2133
|
-
// signing -- so no CRL from this issuer can be authoritative.
|
|
2134
1425
|
var signerAuthorized = true;
|
|
2135
1426
|
if (issuer && issuer.issuerCert) {
|
|
2136
1427
|
var iku;
|
|
@@ -2141,38 +1432,14 @@ function crlChecker(crls, opts) {
|
|
|
2141
1432
|
if (iku && iku.value.cRLSign !== true) signerAuthorized = false;
|
|
2142
1433
|
}
|
|
2143
1434
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
// authoritative, in-scope, current, verified CRL is revoked; the cert is
|
|
2147
|
-
// "good" only when the CRLs consulted together cover ALL EIGHT revocation
|
|
2148
|
-
// reasons (sec. 6.3.3(l) + the termination rule at @5291) and none list it.
|
|
2149
|
-
//
|
|
2150
|
-
// Note there is deliberately no early exit once the mask is complete: the
|
|
2151
|
-
// shipped checker scans every CRL so a clean one cannot shadow a revoking
|
|
2152
|
-
// one, and stopping at full coverage would reintroduce exactly that.
|
|
2153
|
-
var certStatus = null; // sec. 6.3.2(b) cert_status; null = UNREVOKED
|
|
2154
|
-
var reasonsMask = 0; // sec. 6.3.2(a) reasons_mask; the empty set
|
|
1435
|
+
var certStatus = null;
|
|
1436
|
+
var reasonsMask = 0;
|
|
2155
1437
|
var releasedByUnmergedDelta = false;
|
|
2156
|
-
// A CURRENT, authoritative delta that merged with nothing means the local
|
|
2157
|
-
// revocation picture is incomplete: the base it names may be newer than any
|
|
2158
|
-
// complete CRL held here, so a clean base proves less than it appears to.
|
|
2159
|
-
// The shipped checker blocks on exactly this, and the merge must only
|
|
2160
|
-
// ever turn undetermined INTO good -- never make an unmerged delta weaker
|
|
2161
|
-
// than it was before this feature existed.
|
|
2162
1438
|
var sawUnmergedDelta = false;
|
|
2163
1439
|
var classified = classifyCrls(parsed);
|
|
2164
1440
|
var consumedDeltas = [];
|
|
2165
1441
|
|
|
2166
|
-
// Every gate a CRL must pass before it may speak for this certificate, in
|
|
2167
|
-
// the shipped order. Returns null when the CRL is unusable, otherwise the
|
|
2168
|
-
// interim_reasons_mask it may contribute -- 0 meaning "consulted for
|
|
2169
|
-
// revocation only", which is how a non-corresponding shard is honestly
|
|
2170
|
-
// encoded (it can reveal a revocation but can never establish coverage).
|
|
2171
1442
|
async function gateCrl(rec) {
|
|
2172
|
-
// Memoized per RECORD, and the records are rebuilt by classifyCrls on every check(), so the
|
|
2173
|
-
// cache is per-call and the checker stays re-entrant. Without it the base-by-delta pairing
|
|
2174
|
-
// below would repeat an asynchronous public-key signature verification for every pair --
|
|
2175
|
-
// O(completes x deltas) verifications where the shipped loop did O(crls).
|
|
2176
1443
|
if (rec._gated) return rec._gate;
|
|
2177
1444
|
rec._gate = await gateCrlUncached(rec);
|
|
2178
1445
|
rec._gated = true;
|
|
@@ -2180,48 +1447,25 @@ function crlChecker(crls, opts) {
|
|
|
2180
1447
|
}
|
|
2181
1448
|
async function gateCrlUncached(rec) {
|
|
2182
1449
|
var theCrl = rec.crl;
|
|
2183
|
-
// dnEqual throws on a DN carrying an embedded NUL/control byte (CVE-2009-2408).
|
|
2184
|
-
// A single malformed CRL in the bundle must NOT abort the whole check (which
|
|
2185
|
-
// would mask a later authoritative CRL and pass under softFail) -- treat it
|
|
2186
|
-
// as unusable and skip it, consulting the remaining CRLs.
|
|
2187
1450
|
var issuerMatches;
|
|
2188
1451
|
try { issuerMatches = dnEqual(theCrl.issuer.rdns, cert.issuer.rdns); }
|
|
2189
1452
|
catch (_e) { return null; }
|
|
2190
1453
|
if (!issuerMatches) return null;
|
|
2191
1454
|
if (!signerAuthorized) return null;
|
|
2192
1455
|
|
|
2193
|
-
// A validly-signed CRL carrying a CRITICAL extension this checker does
|
|
2194
|
-
// not understand (anything but issuingDistributionPoint / deltaCRLIndicator)
|
|
2195
|
-
// may change the CRL's scope or meaning -- treat it as unusable (RFC 5280
|
|
2196
|
-
// sec. 5.2 critical-extension semantics), never authoritative.
|
|
2197
1456
|
var unhandledCritical = false;
|
|
2198
1457
|
for (var x = 0; x < theCrl.crlExtensions.length; x++) {
|
|
2199
1458
|
var xe = theCrl.crlExtensions[x];
|
|
2200
1459
|
if (xe.critical && xe.oid !== OID_IDP && xe.oid !== OID_DELTA_CRL) { unhandledCritical = true; break; }
|
|
2201
1460
|
}
|
|
2202
|
-
// RFC 5280 sec. 5.3: a critical CRL-ENTRY extension the checker cannot
|
|
2203
|
-
// process (anything but reasonCode) makes the CRL unusable for ANY
|
|
2204
|
-
// certificate, not just the entry that carries it.
|
|
2205
1461
|
for (var ry = 0; ry < theCrl.revokedCertificates.length && !unhandledCritical; ry++) {
|
|
2206
|
-
// Coverage residual -- the `|| []` fallback is unreachable: schema-crl sets
|
|
2207
|
-
// crlEntryExtensions to [] when absent, so it is always an array.
|
|
2208
1462
|
var ees = theCrl.revokedCertificates[ry].crlEntryExtensions || [];
|
|
2209
1463
|
for (var ex = 0; ex < ees.length; ex++) {
|
|
2210
|
-
// Key on the stable OID only -- a display name is registry-dependent
|
|
2211
|
-
// (a custom OID could be registered as "reasonCode"), so matching by
|
|
2212
|
-
// name would let an unhandled critical entry extension fail open.
|
|
2213
1464
|
if (ees[ex].critical && ees[ex].oid !== OID_REASON_CODE) { unhandledCritical = true; break; }
|
|
2214
1465
|
}
|
|
2215
1466
|
}
|
|
2216
1467
|
if (unhandledCritical) return null;
|
|
2217
1468
|
|
|
2218
|
-
// sec. 6.3.3(d): the interim reason mask this CRL contributes. Its two
|
|
2219
|
-
// inputs are the IDP's onlySomeReasons and the CORRESPONDING certificate
|
|
2220
|
-
// DistributionPoint's reasons -- never an entry's reasonCode (sec. 5.2.5
|
|
2221
|
-
// @3628 explicitly permits a shard entry to omit one).
|
|
2222
|
-
// Set when the shard covers no reasons for this certificate. It is recorded rather than
|
|
2223
|
-
// RETURNED here: the currency and signature gates below still have to run, or an expired,
|
|
2224
|
-
// not-yet-valid or FORGED shard could be scanned for revocations and falsely revoke.
|
|
2225
1469
|
var noCoverage = false;
|
|
2226
1470
|
var idpMask = null, dpMask = null, sawIdp = false;
|
|
2227
1471
|
var idpExtension = null;
|
|
@@ -2229,81 +1473,39 @@ function crlChecker(crls, opts) {
|
|
|
2229
1473
|
if (idpExtension) {
|
|
2230
1474
|
sawIdp = true;
|
|
2231
1475
|
var idp = decodeIdp(idpExtension);
|
|
2232
|
-
if (idp.malformed) return null;
|
|
2233
|
-
// An indirect CRL carries entries for other issuers keyed by the
|
|
2234
|
-
// per-entry certificateIssuer attribute (not tracked here) -- matching
|
|
2235
|
-
// by serial alone could revoke the wrong cert or falsely cover it, so
|
|
2236
|
-
// treat an indirect CRL as unusable until certificateIssuer is honored.
|
|
1476
|
+
if (idp.malformed) return null;
|
|
2237
1477
|
if (idp.indirect) return null;
|
|
2238
|
-
if (idp.onlyAttr) return null;
|
|
2239
|
-
if (idp.onlyCa && certIsCa !== true) return null;
|
|
1478
|
+
if (idp.onlyAttr) return null;
|
|
1479
|
+
if (idp.onlyCa && certIsCa !== true) return null;
|
|
2240
1480
|
if (idp.onlyUser && certIsCa !== false) return null;
|
|
2241
|
-
// The same fail-closed decision the distributionPoint correspondence rests on (sec. 5.2.5
|
|
2242
|
-
// @3601 lets a relying party not support the IDP at all): a scope a non-supporting
|
|
2243
|
-
// verifier would IGNORE is not a scope to build coverage on. So a non-critical IDP's
|
|
2244
|
-
// onlySomeReasons contributes nothing -- which also preserves the shipped property that
|
|
2245
|
-
// an onlySomeReasons shard could only ever WITHHOLD good, never establish it.
|
|
2246
1481
|
if (idpExtension.critical === true) idpMask = idp.onlySomeReasonsMask;
|
|
2247
1482
|
else if (idp.onlySomeReasons) noCoverage = true;
|
|
2248
1483
|
if (idp.hasDistributionPoint) {
|
|
2249
|
-
// RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
|
|
2250
|
-
// certificate only when the IDP's distribution point shares at
|
|
2251
|
-
// least one IDENTICALLY-ENCODED name with one of the certificate's
|
|
2252
|
-
// own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
|
|
2253
|
-
// be used in the distributionPoint fields of the certificate and
|
|
2254
|
-
// the CRL"). The IDP must also be CRITICAL to be relied on for
|
|
2255
|
-
// scope: sec. 5.2.5 describes the IDP as "a critical CRL extension"
|
|
2256
|
-
// (descriptive phrasing, not an imperative MUST -- and @3601 lets a
|
|
2257
|
-
// relying party not support it at all), so building coverage on a
|
|
2258
|
-
// scope a non-supporting verifier would ignore is a deliberate
|
|
2259
|
-
// fail-closed decision. A non-corresponding shard contributes NO
|
|
2260
|
-
// coverage but is still consulted for revocation below: serials are
|
|
2261
|
-
// unique per issuer, so a listed serial is a genuine revocation.
|
|
2262
1484
|
var matchedDp = idpExtension.critical === true
|
|
2263
1485
|
? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
|
|
2264
1486
|
: null;
|
|
2265
1487
|
if (!matchedDp) noCoverage = true;
|
|
2266
|
-
// sec. 6.3.3(d)(1)/(d)(3): a matched DP carrying `reasons` bounds the
|
|
2267
|
-
// interim mask. A present-but-unreadable value cannot bound anything
|
|
2268
|
-
// safely, so it contributes nothing rather than defaulting open.
|
|
2269
1488
|
else if (matchedDp.reasons) {
|
|
2270
1489
|
dpMask = reasonMaskFromBitString(matchedDp.reasons);
|
|
2271
1490
|
if (dpMask === null) noCoverage = true;
|
|
2272
1491
|
}
|
|
2273
1492
|
}
|
|
2274
1493
|
}
|
|
2275
|
-
if (guard.time.instantOf(theCrl.thisUpdate) > guard.time.instantOf(time)) return null;
|
|
2276
|
-
// A CRL with no nextUpdate has no bounded validity -- its currency
|
|
2277
|
-
// cannot be confirmed (RFC 5280 sec. 5.1.2.5 requires nextUpdate), so a
|
|
2278
|
-
// replayed old CRL must not read "good". Treat it as unusable.
|
|
1494
|
+
if (guard.time.instantOf(theCrl.thisUpdate) > guard.time.instantOf(time)) return null;
|
|
2279
1495
|
if (!theCrl.nextUpdate ||
|
|
2280
|
-
guard.time.instantOf(theCrl.nextUpdate) < guard.time.instantOf(time)) return null;
|
|
1496
|
+
guard.time.instantOf(theCrl.nextUpdate) < guard.time.instantOf(time)) return null;
|
|
2281
1497
|
|
|
2282
1498
|
var sigOk = await crlVerify.verifyCrlSignature(theCrl, issuer.workingPublicKey);
|
|
2283
|
-
if (!sigOk) return null;
|
|
1499
|
+
if (!sigOk) return null;
|
|
2284
1500
|
|
|
2285
|
-
// sec. 6.3.3 @5295: a CRL not named by any distribution point is processed
|
|
2286
|
-
// as though under a DP whose `reasons` and `cRLIssuer` are absent -- i.e.
|
|
2287
|
-
// all-reasons. That is why a full-scope CRL still covers a certificate
|
|
2288
|
-
// that happens to carry a cRLDistributionPoints extension.
|
|
2289
1501
|
void sawIdp;
|
|
2290
1502
|
return { interim: noCoverage ? 0 : interimReasonMask(idpMask, dpMask) };
|
|
2291
1503
|
}
|
|
2292
1504
|
|
|
2293
|
-
// sec. 6.3.3(i)/(j): the certificate's entry on one CRL, as a CRLReason
|
|
2294
|
-
// value, or null for UNREVOKED. sec. 6.3.3(i)(2): an entry with no
|
|
2295
|
-
// reasonCode extension is `unspecified` (0), which is a revocation.
|
|
2296
1505
|
function scanCrl(theCrl) {
|
|
2297
1506
|
for (var r = 0; r < theCrl.revokedCertificates.length; r++) {
|
|
2298
1507
|
var entry = theCrl.revokedCertificates[r];
|
|
2299
1508
|
if (entry.serialNumberHex !== cert.serialNumberHex) continue;
|
|
2300
|
-
// A revocation is effective as of its revocationDate (RFC 5280 sec. 5.3).
|
|
2301
|
-
// In the DEFAULT present-time validation a listed serial is revoked
|
|
2302
|
-
// regardless of that date -- a future revocationDate is post-dating or
|
|
2303
|
-
// clock skew and must NOT read good. Only under an EXPLICIT historical
|
|
2304
|
-
// validation (opts.historicalMode) -- validating as of a past instant,
|
|
2305
|
-
// e.g. a timestamped signature -- does an entry dated AFTER the
|
|
2306
|
-
// validation time not yet apply.
|
|
2307
1509
|
// allow:nan-date-comparison-unguarded -- revocationDate is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (the skip is not taken -> the entry is treated as revoked), and `time` is validated at the path.validate / crlChecker entry points.
|
|
2308
1510
|
if (historical && guard.time.isDate(entry.revocationDate) &&
|
|
2309
1511
|
guard.time.instantOf(entry.revocationDate) > guard.time.instantOf(time)) continue;
|
|
@@ -2313,16 +1515,6 @@ function crlChecker(crls, opts) {
|
|
|
2313
1515
|
return null;
|
|
2314
1516
|
}
|
|
2315
1517
|
|
|
2316
|
-
// sec. 6.3.3(a)(2): a delta is obtained only when use-deltas is set AND a
|
|
2317
|
-
// locator exists -- freshestCRL on the certificate or on the complete CRL.
|
|
2318
|
-
// sec. 5.2.6 @3719: the locator's CONTENTS are only ever used to find a
|
|
2319
|
-
// delta, never to validate one, so its presence is the whole gate.
|
|
2320
|
-
// PRESENCE only, deliberately. sec. 5.2.6 @3719 says the freshestCRL contents are used to
|
|
2321
|
-
// LOCATE a delta and never to validate one, so decoding them would be using a value the RFC
|
|
2322
|
-
// says not to use. It is also not a security control: enabling the merge grants nothing on its
|
|
2323
|
-
// own, because a delta still has to pass every gate -- issuer, authorization, criticality,
|
|
2324
|
-
// scope, currency and SIGNATURE -- plus the sec. 5.2.4 merge preconditions. An attacker who
|
|
2325
|
-
// could satisfy those already controls the issuing key.
|
|
2326
1518
|
var certHasFreshest = !!findExt(cert, OID_FRESHEST_CRL);
|
|
2327
1519
|
function deltaLocatorPresent(completeRec) {
|
|
2328
1520
|
return certHasFreshest || crlExtValue(completeRec.crl, OID_FRESHEST_CRL) !== null;
|
|
@@ -2333,11 +1525,6 @@ function crlChecker(crls, opts) {
|
|
|
2333
1525
|
var gate = await gateCrl(rec);
|
|
2334
1526
|
if (!gate) continue;
|
|
2335
1527
|
|
|
2336
|
-
// sec. 6.3.3(c): pair this complete CRL with a delta it may be combined
|
|
2337
|
-
// with. The delta passes its OWN gates first -- sec. 6.3.3(h) requires
|
|
2338
|
-
// its signature verified and (f) its issuer authorized, exactly as for a
|
|
2339
|
-
// complete CRL -- so a stale, unverifiable or out-of-scope delta is never
|
|
2340
|
-
// merged.
|
|
2341
1528
|
var chosenDelta = null;
|
|
2342
1529
|
if (useDeltas && deltaLocatorPresent(rec)) {
|
|
2343
1530
|
var candidates = [];
|
|
@@ -2345,12 +1532,6 @@ function crlChecker(crls, opts) {
|
|
|
2345
1532
|
var cand = classified.deltas[di];
|
|
2346
1533
|
if (!deltaMergesWith(cand, rec)) continue;
|
|
2347
1534
|
if (!(await gateCrl(cand))) continue;
|
|
2348
|
-
// Mergeable and usable: this delta's scope IS covered by a merge, even
|
|
2349
|
-
// if sec. 5.2.4 @3580 selection prefers a sibling with a later
|
|
2350
|
-
// thisUpdate. Only a delta that pairs with NO complete CRL leaves the
|
|
2351
|
-
// picture incomplete -- a losing candidate must not block a good result,
|
|
2352
|
-
// or publishing two current deltas (which the RFC permits) would be
|
|
2353
|
-
// worse than publishing one.
|
|
2354
1535
|
cand.accounted = true;
|
|
2355
1536
|
candidates.push(cand);
|
|
2356
1537
|
}
|
|
@@ -2360,35 +1541,20 @@ function crlChecker(crls, opts) {
|
|
|
2360
1541
|
var status;
|
|
2361
1542
|
if (chosenDelta) {
|
|
2362
1543
|
consumedDeltas.push(chosenDelta);
|
|
2363
|
-
status = scanCrl(chosenDelta.crl);
|
|
2364
|
-
if (status === null) status = scanCrl(rec.crl);
|
|
1544
|
+
status = scanCrl(chosenDelta.crl);
|
|
1545
|
+
if (status === null) status = scanCrl(rec.crl);
|
|
2365
1546
|
} else {
|
|
2366
1547
|
status = scanCrl(rec.crl);
|
|
2367
1548
|
}
|
|
2368
|
-
// (k): removeFromCRL means the certificate is no longer revoked. It is
|
|
2369
|
-
// normalized wherever it appears -- sec. 5.3.1's "only in delta CRLs" binds
|
|
2370
|
-
// the CA that emits it, not this consumer, and rejecting a complete CRL
|
|
2371
|
-
// that carries one would make an unusual-but-harmless CRL unusable.
|
|
2372
1549
|
if (status === 8) status = null;
|
|
2373
1550
|
|
|
2374
1551
|
if (status !== null && certStatus === null) certStatus = status;
|
|
2375
|
-
else if (status === null) reasonsMask |= gate.interim;
|
|
1552
|
+
else if (status === null) reasonsMask |= gate.interim;
|
|
2376
1553
|
}
|
|
2377
1554
|
|
|
2378
|
-
// An unmerged delta still speaks, under the shipped fail-closed posture: a
|
|
2379
|
-
// serial it lists is a genuine revocation (serials are unique per issuer),
|
|
2380
|
-
// and a removeFromCRL it carries blocks a definitive revoked without being
|
|
2381
|
-
// able to establish good. This is what keeps the merge MONOTONIC -- it may
|
|
2382
|
-
// turn undetermined into good or revoked, but a delta that merges with
|
|
2383
|
-
// nothing can never erase a revocation the checker would otherwise report.
|
|
2384
1555
|
for (var dj = 0; dj < classified.deltas.length; dj++) {
|
|
2385
1556
|
var dRec = classified.deltas[dj];
|
|
2386
1557
|
if (contains(consumedDeltas, dRec)) continue;
|
|
2387
|
-
// A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
|
|
2388
|
-
// does not speak for its scope at all -- the selected delta does, and it was evaluated
|
|
2389
|
-
// against the base. Letting a superseded delta contribute its revocation while ignoring its
|
|
2390
|
-
// release would be incoherent: an older delta revoking a certificate that the newer one
|
|
2391
|
-
// releases would resurrect the revocation the CA withdrew.
|
|
2392
1558
|
if (dRec.accounted) continue;
|
|
2393
1559
|
if (!(await gateCrl(dRec))) continue;
|
|
2394
1560
|
sawUnmergedDelta = true;
|
|
@@ -2398,9 +1564,6 @@ function crlChecker(crls, opts) {
|
|
|
2398
1564
|
if (certStatus === null) certStatus = dStatus;
|
|
2399
1565
|
}
|
|
2400
1566
|
|
|
2401
|
-
// A delta released this serial from hold but its base was not merged: a
|
|
2402
|
-
// definitive revoked would leave a released certificate rejected, so the
|
|
2403
|
-
// status is undetermined. This outranks a base CRL's revocation.
|
|
2404
1567
|
if (releasedByUnmergedDelta) return { status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" };
|
|
2405
1568
|
if (certStatus !== null) {
|
|
2406
1569
|
var reasonName = constants.NAMES.CRL_REASON[String(certStatus)] || "unspecified";
|
|
@@ -2419,37 +1582,21 @@ function crlChecker(crls, opts) {
|
|
|
2419
1582
|
};
|
|
2420
1583
|
}
|
|
2421
1584
|
|
|
2422
|
-
// The decoded reasonCode of a revoked CRL entry (crl.parse surfaces it as a
|
|
2423
|
-
// number), or null when absent.
|
|
2424
1585
|
var OID_REASON_CODE = oid.byName("reasonCode");
|
|
2425
1586
|
function crlEntryReason(entry) {
|
|
2426
|
-
// Coverage residual -- the `|| []` fallback is unreachable: schema-crl guarantees
|
|
2427
|
-
// crlEntryExtensions is an array (empty when absent).
|
|
2428
1587
|
var exts = entry.crlEntryExtensions || [];
|
|
2429
1588
|
for (var i = 0; i < exts.length; i++) {
|
|
2430
|
-
if (exts[i].oid === OID_REASON_CODE) return exts[i].value;
|
|
1589
|
+
if (exts[i].oid === OID_REASON_CODE) return exts[i].value;
|
|
2431
1590
|
}
|
|
2432
1591
|
return null;
|
|
2433
1592
|
}
|
|
2434
1593
|
|
|
2435
|
-
// Verify a raw signature over tbsBytes with an SPKI public key -- the shared core
|
|
2436
|
-
// of every certificate / CRL / OCSP signature check. Resolve the algorithm
|
|
2437
|
-
// descriptor, enforce the key-OID <-> sig-OID binding (the algorithm-confusion
|
|
2438
|
-
// guard, RFC 9814 sec. 4), import the SPKI, bridge an ECDSA DER signature to
|
|
2439
|
-
// P1363, and verify. Any fault -- an unresolvable/forbidden algorithm, a
|
|
2440
|
-
// key/sig mismatch, an import or verify failure -- resolves false: a signature
|
|
2441
|
-
// check never throws out of this path, it fails closed. `rawSig` is the raw
|
|
2442
|
-
// signature octets (the caller has already unwrapped any BIT STRING and rejected
|
|
2443
|
-
// a non-octet-aligned one).
|
|
2444
1594
|
function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
2445
1595
|
var d;
|
|
2446
1596
|
try {
|
|
2447
1597
|
d = resolveDescriptor(sigAlg);
|
|
2448
1598
|
assertKeyMatchesSigAlg(spkiBytes, sigAlg.oid, d);
|
|
2449
1599
|
} catch (_e) { return Promise.resolve(false); }
|
|
2450
|
-
// A composite-signed CRL / OCSP response verifies through the same combinator
|
|
2451
|
-
// (both halves must pass) that the certificate path uses -- one composite verify,
|
|
2452
|
-
// never a second parallel one.
|
|
2453
1600
|
if (d.composite) return compositeSig.compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature").then(function (r) { return r.ok === true; });
|
|
2454
1601
|
return _importVerifyKey(spkiBytes, d).then(function (key) {
|
|
2455
1602
|
var sig = rawSig;
|
|
@@ -2458,34 +1605,17 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
|
2458
1605
|
}).then(function (ok) { return ok === true; }, function () { return false; });
|
|
2459
1606
|
}
|
|
2460
1607
|
|
|
2461
|
-
// Inject this validator's signature engine into the shared internal crl-verify seam, so pki.crl.verify and
|
|
2462
|
-
// pki.path.crlChecker both route through this ONE engine (no second, weaker CRL verifier) without exposing
|
|
2463
|
-
// the seam on the public pki.path surface.
|
|
2464
1608
|
crlVerify.setEngine(_verifyWithSpki);
|
|
2465
1609
|
|
|
2466
|
-
// The same seam for PKCS#10 proof of possession, so pki.csr.verify checks an inbound certification
|
|
2467
|
-
// request through this ONE engine rather than the producing side's self-check, which waives the
|
|
2468
|
-
// EdDSA low-order-point gate because it runs over a key the caller already controls.
|
|
2469
1610
|
csrVerify.setEngine(_verifyWithSpki);
|
|
2470
1611
|
|
|
2471
|
-
// And for RFC 4211 proof of possession, so an inbound CertReqMsg is checked through the same one
|
|
2472
|
-
// engine as the PKCS#10 form of the same question.
|
|
2473
1612
|
crmfVerify.setEngine(_verifyWithSpki);
|
|
2474
1613
|
|
|
2475
|
-
// And for an RFC 5755 attribute certificate, whose AC issuer is an out-of-path signer reached the
|
|
2476
|
-
// same way.
|
|
2477
1614
|
attrcertVerify.setEngine(_verifyWithSpki);
|
|
2478
1615
|
|
|
2479
|
-
// And for a signed OCSPRequest (RFC 6960 sec. 4.1.1), so pki.ocsp.verifyRequest checks a requestor's
|
|
2480
|
-
// signature through the same one engine that verifies a response, never a second, weaker request path.
|
|
2481
1616
|
ocspRequestVerify.setEngine(_verifyWithSpki);
|
|
2482
1617
|
|
|
2483
|
-
// ---- the OCSP revocation checker (RFC 6960) -------------------------------
|
|
2484
1618
|
|
|
2485
|
-
// The OCSP response-verification core (responder authorization + CertID binding + currency +
|
|
2486
|
-
// status) lives ONCE in lib/ocsp-verify.js, composed here by pki.path.ocspChecker AND by
|
|
2487
|
-
// pki.ocsp.verify -- there is no second, weaker OCSP verify path. This binds the path-validate-
|
|
2488
|
-
// owned signature engine + RFC 5280 cert-profile gates into that shared core.
|
|
2489
1619
|
var ocspCore = ocspVerify.makeOcspVerify({
|
|
2490
1620
|
verifyWithSpki: _verifyWithSpki,
|
|
2491
1621
|
decodeExt: decodeExt, findExt: findExt,
|
|
@@ -2496,25 +1626,8 @@ var ocspCore = ocspVerify.makeOcspVerify({
|
|
|
2496
1626
|
dnEqual: dnEqual,
|
|
2497
1627
|
});
|
|
2498
1628
|
|
|
2499
|
-
// Inject this validator's signature engine + full path build/validate into the internal cmp-verify seam,
|
|
2500
|
-
// so pki.cmp.verify routes incoming CMP signature protection through the SAME engine (never a second, weaker
|
|
2501
|
-
// CMP verifier; never build's self-check, which skips the EdDSA low-order-point gate) and chains an
|
|
2502
|
-
// out-of-path signer certificate through the FULL RFC 5280 sec. 6.1 path validation -- without exposing any
|
|
2503
|
-
// of it on the public pki.path surface (index.js exports the whole path-validate module).
|
|
2504
1629
|
cmpVerify.setEngine({ verifyWithSpki: _verifyWithSpki, build: build, validate: validate });
|
|
2505
|
-
// pki.cmp.session validates the ISSUED leaf certificate (its signature + chain) through the same engine.
|
|
2506
|
-
// `verifyWithSpki` so a root CA key update's rollover signatures are checked by the same signature
|
|
2507
|
-
// engine (with its EdDSA low-order-point and algorithm-confusion gates) that verifies a message's
|
|
2508
|
-
// protection, rather than a second, weaker check inside the session.
|
|
2509
1630
|
cmpSession.setEngine({ build: build, validate: validate, toAnchor: toAnchor, coerceCert: coerceCert, verifyWithSpki: _verifyWithSpki });
|
|
2510
|
-
// pki.cms.verify chains a SignedData's signer certificate to the anchors the CALLER named, so its
|
|
2511
|
-
// `trusted` is decided by this one path engine rather than a second, weaker walk of its own.
|
|
2512
|
-
// `toAnchor` so cms.verify can validate the caller's anchors ONCE at entry, before any signer is
|
|
2513
|
-
// walked -- otherwise a message whose signers all failed would never reach a build call and a
|
|
2514
|
-
// malformed anchor would pass unnoticed.
|
|
2515
|
-
// `resolvePurposeOpts` so cms.verify can reject a malformed requiredEku / checkPurpose at ITS entry
|
|
2516
|
-
// point, through the SAME definition the walk uses -- a message whose signers all failed never
|
|
2517
|
-
// reaches a build call, and a caller's configuration must not be judged by the message's quality.
|
|
2518
1631
|
cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
|
|
2519
1632
|
resolvePurposeOpts: resolvePurposeOpts, assertAnchorConstraints: assertAnchorConstraints });
|
|
2520
1633
|
|
|
@@ -2549,17 +1662,11 @@ cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
|
|
|
2549
1662
|
* typeof checker.check; // "function"
|
|
2550
1663
|
*/
|
|
2551
1664
|
function ocspChecker(responses) {
|
|
2552
|
-
// The same door verifyOcspResponse uses. This checker's responses reach the identical three-part
|
|
2553
|
-
// signature check, so accepting a parsed object here on a truthy responseStatus would leave the
|
|
2554
|
-
// door shut on one of the two ways into that check and open on the other.
|
|
2555
1665
|
var parsed = (responses || []).map(function (r) { return _ocspFromBytes(r); });
|
|
2556
1666
|
return {
|
|
2557
1667
|
check: async function (cert, issuer, ctx) {
|
|
2558
1668
|
var time = ctx.time;
|
|
2559
1669
|
var historical = ctx.historicalMode === true;
|
|
2560
|
-
// Issuer DN candidates to match the CertID against (RFC 6960 sec. 4.1.1 names
|
|
2561
|
-
// the checked cert's issuer field; a response MAY instead carry the issuer
|
|
2562
|
-
// certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
|
|
2563
1670
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2564
1671
|
function addNameCandidate(nm) {
|
|
2565
1672
|
if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
|
|
@@ -2570,12 +1677,6 @@ function ocspChecker(responses) {
|
|
|
2570
1677
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuer.workingPublicKey); }
|
|
2571
1678
|
catch (_e) { return { status: "unknown", reason: "the issuer public key could not be read to recompute the OCSP CertID" }; }
|
|
2572
1679
|
|
|
2573
|
-
// A serial is revoked if ANY authoritative, verified, current response says
|
|
2574
|
-
// so -- a clean response must never shadow a revoking one (the crlChecker
|
|
2575
|
-
// fail-closed law). "good" needs at least one authoritative match; every
|
|
2576
|
-
// other outcome is undetermined. The shared verify core evaluates each
|
|
2577
|
-
// response (responder authorization + signature + CertID + currency +
|
|
2578
|
-
// status) and returns a per-response summary this aggregates.
|
|
2579
1680
|
var revokedResult = null;
|
|
2580
1681
|
var sawGood = false;
|
|
2581
1682
|
var sawUnknownStatus = false;
|
|
@@ -2662,26 +1763,12 @@ function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
|
2662
1763
|
var parsedResponse, subject, issuer;
|
|
2663
1764
|
try {
|
|
2664
1765
|
parsedResponse = _ocspFromBytes(response);
|
|
2665
|
-
// The two certificates go through the same door as the response, for the same reason: this
|
|
2666
|
-
// verdict is about a certificate's IDENTITY -- its serial and its issuer's name and key are
|
|
2667
|
-
// what the CertID is matched against, and the issuer's key is what authorizes a delegated
|
|
2668
|
-
// responder. A caller-assembled certificate could name one identity while carrying another's
|
|
2669
|
-
// signed bytes, so both are re-derived from the bytes their parser read.
|
|
2670
1766
|
subject = coerceCert(cert, "the certificate");
|
|
2671
1767
|
issuer = coerceCert(issuerCert, "the issuer certificate");
|
|
2672
1768
|
} catch (e) { return Promise.reject(e); }
|
|
2673
1769
|
return _verifyOcspParsed(parsedResponse, subject, issuer, time, opts);
|
|
2674
1770
|
}
|
|
2675
1771
|
|
|
2676
|
-
// The response, always parsed from the bytes the caller handed over.
|
|
2677
|
-
//
|
|
2678
|
-
// A signature check has three parts -- the signature, the algorithm that verifies it, and the byte
|
|
2679
|
-
// range it covers -- and on a claimed-parsed response all three are separate properties of one
|
|
2680
|
-
// caller-supplied object. Pair a real CA's signature over a certificate IT issued with that
|
|
2681
|
-
// certificate's own tbsBytes and algorithm, label them `signature` / `tbsResponseDataBytes` /
|
|
2682
|
-
// `signatureAlgorithm`, and the check verifies: a genuine signature, over the bytes it was made
|
|
2683
|
-
// over, under the right key -- for a structure the responder never produced. Parsing binds the three
|
|
2684
|
-
// to one byte string, which is the only thing that makes the verdict about a response at all.
|
|
2685
1772
|
var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
|
|
2686
1773
|
function _ocspFromBytes(response) {
|
|
2687
1774
|
return guard.parsed.fromTrustedSource(response, "ocspResponse", _OCSP_CLAIM, function (bytes) {
|
|
@@ -2693,28 +1780,15 @@ function _ocspFromBytes(response) {
|
|
|
2693
1780
|
"verifyOcspResponse: the response must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signature, the algorithm that verifies it and the bytes it covers are separate properties of a parsed object, so a REBUILT response (Object.assign, spread, a JSON round-trip) could have the three describe different responses and is refused");
|
|
2694
1781
|
}
|
|
2695
1782
|
|
|
2696
|
-
// The verification over a response this module has ALREADY derived from bytes -- reached only
|
|
2697
|
-
// through the door above and through ocspChecker, which parses the responder's own reply. Not
|
|
2698
|
-
// exported: this module's export object IS pki.path, so anything on it is public surface and frozen
|
|
2699
|
-
// by the API snapshot, whatever a comment beside it claims.
|
|
2700
1783
|
function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
2701
1784
|
opts = opts || {};
|
|
2702
|
-
// The currency + responder-cert validity windows compare against `time`; a missing or invalid
|
|
2703
|
-
// check date must fail closed (a NaN compares false against every bound), never silently pass.
|
|
2704
1785
|
if (!guard.time.isDate(time) || isNaN(guard.time.instantOf(time))) {
|
|
2705
1786
|
return Promise.reject(E("path/bad-input", "verifyOcspResponse: time must be a valid Date (the currency + responder-validity check date)"));
|
|
2706
1787
|
}
|
|
2707
|
-
// Bind the supplied issuer certificate to the target: the target's issuer DN must equal the
|
|
2708
|
-
// issuer cert's subject DN AND the target's signature must verify under the issuer's key. A
|
|
2709
|
-
// direct-CA responder is authorized by exactly this issuer identity and the CertID is recomputed
|
|
2710
|
-
// under the issuer's key, so without the cryptographic binding a rogue certificate sharing the
|
|
2711
|
-
// issuer's subject DN (but a different key) could recompute a matching CertID and sign a "good"
|
|
2712
|
-
// response for a certificate that CA never issued. ocspChecker gets an issuer already chained by
|
|
2713
|
-
// the path validator; the standalone entry must establish the binding itself. Fail closed.
|
|
2714
1788
|
function unbound(reason) { return { status: "unknown", responderAuthorized: false, signatureValid: false, matched: false, reason: reason }; }
|
|
2715
1789
|
var boundName;
|
|
2716
1790
|
try { boundName = dnEqual(cert.issuer.rdns, issuerCert.subject.rdns); }
|
|
2717
|
-
catch (e) { return Promise.reject(e); }
|
|
1791
|
+
catch (e) { return Promise.reject(e); }
|
|
2718
1792
|
if (!boundName) {
|
|
2719
1793
|
return Promise.resolve(unbound("the supplied issuer certificate's subject does not match the target certificate's issuer"));
|
|
2720
1794
|
}
|
|
@@ -2735,91 +1809,32 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2735
1809
|
});
|
|
2736
1810
|
}
|
|
2737
1811
|
|
|
2738
|
-
// ---- certification path BUILDING (pki.path.build, RFC 4158) ----------------
|
|
2739
1812
|
|
|
2740
|
-
// A soft extension decode for the RFC 4158 sec. 3.5 SORT hints (AKI/SKI/
|
|
2741
|
-
// basicConstraints/keyUsage): a present-but-undecodable extension on an
|
|
2742
|
-
// UNTRUSTED pool candidate degrades to "no hint", never a hard fail -- the sort
|
|
2743
|
-
// weight is advisory, the branch is still tried, and validate is the authority.
|
|
2744
1813
|
function softDecode(cert, extOid) {
|
|
2745
1814
|
try { return decodeExt(cert, extOid); }
|
|
2746
1815
|
catch (_e) { return null; }
|
|
2747
1816
|
}
|
|
2748
1817
|
|
|
2749
|
-
// dnEqual that fails a control-byte DN (CVE-2009-2408) closed to "not a match"
|
|
2750
|
-
// rather than rejecting the whole build -- one malformed pool cert must not
|
|
2751
|
-
// poison a buildable path (parity with selfIssued's swallow). The hard
|
|
2752
|
-
// name-chaining gate remains validate's own dnEqual over the chosen path.
|
|
2753
1818
|
function nameMatchSoft(rdnsA, rdnsB) {
|
|
2754
1819
|
try { return dnEqual(rdnsA, rdnsB); }
|
|
2755
1820
|
catch (_e) { return false; }
|
|
2756
1821
|
}
|
|
2757
1822
|
|
|
2758
|
-
// A certificate reaching a VERDICT is re-derived from the bytes the parser read, never trusted as
|
|
2759
|
-
// the object it arrives as. Completeness -- every field present with the right type -- is not enough
|
|
2760
|
-
// here, because a certificate's meaning is one signature over one byte range while a parsed
|
|
2761
|
-
// certificate presents that range, the signature, and the fields the range encodes as separate
|
|
2762
|
-
// properties. Keep a real CA certificate's `tbsBytes` and signature and substitute only its
|
|
2763
|
-
// `subjectPublicKeyInfo`, and every completeness rule passes, this walk verifies the ORIGINAL signed
|
|
2764
|
-
// range, and then uses the substituted key to check the next certificate -- a forged chain built out
|
|
2765
|
-
// of a genuine certificate. Emptying `extensions` is the same move against basicConstraints,
|
|
2766
|
-
// keyUsage, the name constraints and the unknown-critical rule.
|
|
2767
|
-
//
|
|
2768
|
-
// So the door takes the parser's record instead: a certificate from pki.schema.x509.parse re-parses
|
|
2769
|
-
// from the bytes it was read from, and anything done to that object since is discarded. One a caller
|
|
2770
|
-
// assembled has no record and is refused rather than silently believed.
|
|
2771
1823
|
function coerceCert(input, label) {
|
|
2772
1824
|
return guard.parsed.acceptDerived(input, "certificate", x509.parse, E, "path/bad-input",
|
|
2773
1825
|
label || "a certificate");
|
|
2774
1826
|
}
|
|
2775
1827
|
|
|
2776
|
-
// A trust-store entry is either a ready anchor tuple { name, publicKey,
|
|
2777
|
-
// algorithm, parameters? } or a self-signed root certificate reduced to that
|
|
2778
|
-
// tuple. The algorithm is the SPKI KEY-algorithm OID (the sec. 6.1.4(f)
|
|
2779
|
-
// parameter-inheritance value), mirroring trust.js _mkAnchor -- NOT the
|
|
2780
|
-
// signature OID. The anchor is an input to validate, never one of the path certs.
|
|
2781
|
-
// The two key-purpose options, resolved and typo-checked. Extracted so a CALLER can validate them
|
|
2782
|
-
// at ITS entry point rather than only when a path is actually walked: a format verb that skips the
|
|
2783
|
-
// walk -- pki.cms.verify does when no signer verified -- would otherwise accept a malformed
|
|
2784
|
-
// purpose in silence, making configuration validity depend on the message. One definition, so the
|
|
2785
|
-
// answer cannot drift between the caller's early check and the walk's own.
|
|
2786
|
-
//
|
|
2787
|
-
// `requiredEku` gates the TARGET certificate's own EKU extension; `checkPurpose` selects which
|
|
2788
|
-
// per-purpose key the ANCHOR's NSS trust metadata (purposes / distrustAfter) is consulted under.
|
|
2789
|
-
// They are independent, and each is a registered OID name or a canonical dotted OID.
|
|
2790
|
-
// An anchor's CONSTRAINT metadata for one purpose, validated fail-closed and returned normalized.
|
|
2791
|
-
// A PRESENT-but-malformed distrustAfter (an Invalid Date: instanceof Date yet a NaN time) would
|
|
2792
|
-
// make `notBefore > it` NaN-false and SILENTLY drop the distrust restriction -- the NaN-Date
|
|
2793
|
-
// fail-open. Absent metadata is no restriction and returns null.
|
|
2794
|
-
//
|
|
2795
|
-
// Separate from resolvePurposeOpts because it validates the ANCHOR rather than the options, and
|
|
2796
|
-
// exposed for the same reason: a caller that may never reach the walk -- pki.cms.verify when no
|
|
2797
|
-
// signer verified -- has to be able to reject a malformed anchor at ITS entry point, through this
|
|
2798
|
-
// same definition, so configuration validity never depends on the message.
|
|
2799
|
-
// Does this anchor carry trust metadata that only a named key purpose can unlock? A non-empty
|
|
2800
|
-
// `distrustAfter` or `purposes` map is such metadata -- both are indexed BY purpose, so both are
|
|
2801
|
-
// inert without one. An EMPTY map states no constraint and is not a reason to refuse.
|
|
2802
1828
|
function _hasPurposeScopedMetadata(ta) {
|
|
2803
1829
|
if (!ta || typeof ta !== "object") return false;
|
|
2804
1830
|
return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
|
|
2805
1831
|
var m = ta[k];
|
|
2806
|
-
// Own names, not Object.keys: a non-enumerable own entry is still a restriction and must count, so it cannot
|
|
2807
|
-
// evade this "the anchor carries purpose-scoped metadata -> require checkPurpose" gate. An inherited map
|
|
2808
|
-
// (Object.create(base) or a polluted Object.prototype) is already refused by toAnchor before an anchor
|
|
2809
|
-
// reaches here, so this read does not resolve an inherited value.
|
|
2810
1832
|
return !!m && typeof m === "object" && intrinsic.getOwnPropertyNames(m).length > 0;
|
|
2811
1833
|
});
|
|
2812
1834
|
}
|
|
2813
1835
|
|
|
2814
1836
|
function assertAnchorConstraints(ta, checkPurpose) {
|
|
2815
1837
|
if (!checkPurpose || !ta) return null;
|
|
2816
|
-
// Read distrustAfter ONCE into a local: reading the anchor field for the presence gate and again for the
|
|
2817
|
-
// value would let an accessor-backed field answer the two reads differently and slip the distrust control.
|
|
2818
|
-
// The normalized anchor already materializes this to plain data; reading once holds the guarantee for any
|
|
2819
|
-
// caller and keeps this from being a live-getter multi-read even if a future path skips normalization. An
|
|
2820
|
-
// inherited distrustAfter (Object.create(base), or a polluted Object.prototype) is already refused by toAnchor
|
|
2821
|
-
// -- _captureConstraintMap / the certificate branch reject a `"distrustAfter" in entry` that is not own -- so
|
|
2822
|
-
// this read never resolves an inherited value on an anchor that reached here.
|
|
2823
1838
|
var da = ta.distrustAfter;
|
|
2824
1839
|
var d = (da && intrinsic.hasOwn(da, checkPurpose)) ? da[checkPurpose] : null;
|
|
2825
1840
|
if (d == null) return null;
|
|
@@ -2834,9 +1849,6 @@ function resolvePurposeOpts(opts) {
|
|
|
2834
1849
|
}
|
|
2835
1850
|
requiredEku = opts.requiredEku.map(function (p) {
|
|
2836
1851
|
if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
|
|
2837
|
-
// A dotted-form attempt (leads with a digit) must be a canonical OID -- a
|
|
2838
|
-
// loose regex accepted a leading-zero / out-of-bounds key that would never
|
|
2839
|
-
// match the canonical EKU the target advertises; anything else is a name.
|
|
2840
1852
|
if (_startsWithDigit(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
|
|
2841
1853
|
var dotted = oid.byName(p);
|
|
2842
1854
|
if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
|
|
@@ -2850,7 +1862,7 @@ function resolvePurposeOpts(opts) {
|
|
|
2850
1862
|
}
|
|
2851
1863
|
if (_startsWithDigit(opts.checkPurpose)) {
|
|
2852
1864
|
var cpDotted = guard.identifier.assertCanonicalOid(opts.checkPurpose, E, "path/bad-input", "validate: opts.checkPurpose");
|
|
2853
|
-
checkPurpose = oid.name(cpDotted) || cpDotted;
|
|
1865
|
+
checkPurpose = oid.name(cpDotted) || cpDotted;
|
|
2854
1866
|
} else {
|
|
2855
1867
|
if (typeof oid.byName(opts.checkPurpose) !== "string") throw E("path/bad-input", "validate: opts.checkPurpose " + JSON.stringify(opts.checkPurpose) + " is not a registered OID name");
|
|
2856
1868
|
checkPurpose = opts.checkPurpose;
|
|
@@ -2859,35 +1871,11 @@ function resolvePurposeOpts(opts) {
|
|
|
2859
1871
|
return { requiredEku: requiredEku, checkPurpose: checkPurpose };
|
|
2860
1872
|
}
|
|
2861
1873
|
|
|
2862
|
-
// Snapshot a trust-anchor purposes / distrustAfter map (purpose name -> boolean or Date) to a fresh plain
|
|
2863
|
-
// object, reading each own entry from its DATA DESCRIPTOR -- never by property access. Reading by value would
|
|
2864
|
-
// invoke an accessor entry (a `get serverAuth()`), which is caller code that could mutate the sibling map
|
|
2865
|
-
// before it is captured, so an accessor entry is REFUSED instead. A later getter cannot mutate the fresh copy.
|
|
2866
|
-
// A distrustAfter entry is a Date: copy it BY VALUE (guard.time.instantOf via the intrinsic) so a later
|
|
2867
|
-
// setTime() cannot move the cutoff. Entry enumerability is preserved so the purpose gates see the same shape.
|
|
2868
1874
|
function _snapshotConstraintMap(m, E, code, who) {
|
|
2869
1875
|
var out = {};
|
|
2870
|
-
// getOwnPropertyNames below reads only OWN keys, so a restriction reached through the prototype --
|
|
2871
|
-
// Object.create({ serverAuth: pastDate }) -- would be silently dropped and its cutoff lost. Require the map to
|
|
2872
|
-
// be a plain object of THIS realm: its prototype is Object.prototype (the common case) or null. Anything else
|
|
2873
|
-
// -- a custom prototype, a null-prototype object carrying entries, a Proxy prototype, or a map from another
|
|
2874
|
-
// realm -- is refused, so a prototype-reached restriction is never silently dropped. Only IDENTITY is sound
|
|
2875
|
-
// here: a hostile object can mimic every STRUCTURAL signal of a genuine Object.prototype (a non-enumerable
|
|
2876
|
-
// data entry is indistinguishable from a built-in), so a structural "looks plain" test always leaves a bypass.
|
|
2877
|
-
// A cross-realm map is therefore refused fail-closed -- the operator passes a same-realm plain object (what
|
|
2878
|
-
// pki.trust.parseCertdata emits, or a literal in their own code), which is the norm for this security-critical
|
|
2879
|
-
// input. A name planted on Object.prototype ITSELF is still tolerated (proto === ObjectProto): the map is plain
|
|
2880
|
-
// and consumption reads OWN entries only (the hasOwn gates), so global pollution is ignored, not read. m is
|
|
2881
|
-
// non-Proxy (refused at capture) and this is a reference comparison, so no getPrototypeOf / ownKeys trap runs
|
|
2882
|
-
// even when the prototype is a Proxy.
|
|
2883
1876
|
var proto = intrinsic.getPrototypeOf(m);
|
|
2884
1877
|
if (proto !== null && proto !== intrinsic.ObjectProto) throw E(code, who + ": a trustAnchor constraint map must be a plain object (its own entries only), not one with a custom or cross-realm prototype");
|
|
2885
1878
|
var keys = intrinsic.getOwnPropertyNames(m);
|
|
2886
|
-
// A constraint map is plain STRING-keyed data. A symbol-keyed entry (getOwnPropertyNames omits symbols) or an
|
|
2887
|
-
// own __proto__ data property (skipped below so it can't pollute the fresh map) would be DROPPED from the
|
|
2888
|
-
// snapshot -- and, keyed by neither a checkPurpose OID name, never applied. Refuse the map rather than silently
|
|
2889
|
-
// drop a caller entry, so the snapshot faithfully carries every restriction or the anchor is refused. ownKeys
|
|
2890
|
-
// (Reflect.ownKeys) counts strings + symbols; more than the string-only names means a symbol key is present.
|
|
2891
1879
|
if (intrinsic.ownKeys(m).length !== keys.length) throw E(code, who + ": a trustAnchor constraint map must not have symbol-keyed entries");
|
|
2892
1880
|
for (var i = 0; i < keys.length; i++) {
|
|
2893
1881
|
if (keys[i] === "__proto__") throw E(code, who + ": a trustAnchor constraint map must not have an own __proto__ entry");
|
|
@@ -2895,29 +1883,17 @@ function _snapshotConstraintMap(m, E, code, who) {
|
|
|
2895
1883
|
if (!d || !intrinsic.hasOwn(d, "value")) throw E(code, who + ": a trustAnchor constraint-map entry must be a data property, not an accessor");
|
|
2896
1884
|
var v = d.value;
|
|
2897
1885
|
if (intrinsic.types.isDate(v)) v = new intrinsic.Date(guard.time.instantOf(v));
|
|
2898
|
-
// Materialize every entry ENUMERABLE. A constraint map is plain data (purpose -> boolean / Date); a caller
|
|
2899
|
-
// entry defined non-enumerable would otherwise be copied non-enumerable and then be invisible to the
|
|
2900
|
-
// Object.keys-based purpose-scoped-metadata gate, letting a restriction slip the "supply checkPurpose" check.
|
|
2901
1886
|
intrinsic.defineProperty(out, keys[i], { value: v, enumerable: true, configurable: true, writable: true });
|
|
2902
1887
|
}
|
|
2903
1888
|
return out;
|
|
2904
1889
|
}
|
|
2905
1890
|
|
|
2906
|
-
// Normalize a captured constraint MAP field for storage. An absent map (null / undefined) carries no
|
|
2907
|
-
// restriction and passes through. A present map MUST be a plain object and is snapshotted; a present non-object
|
|
2908
|
-
// -- a primitive, a Buffer, or (refused inside the snapshot by the plain-prototype check) an array -- cannot hold
|
|
2909
|
-
// a purpose -> boolean/Date restriction, so it is refused rather than passed through as an unusable value that
|
|
2910
|
-
// would silently apply no restriction.
|
|
2911
1891
|
function _normalizeConstraintField(v, E, code, who) {
|
|
2912
1892
|
if (v === null || v === undefined) return v;
|
|
2913
1893
|
if (typeof v !== "object" || intrinsic.isBuffer(v)) throw E(code, who + ": a trustAnchor constraint map must be a plain object");
|
|
2914
1894
|
return _snapshotConstraintMap(v, E, code, who);
|
|
2915
1895
|
}
|
|
2916
1896
|
|
|
2917
|
-
// Capture a trust-anchor constraint MAP field (purposes / distrustAfter) WITHOUT invoking a getter: read its
|
|
2918
|
-
// OWN DATA descriptor. An accessor-backed map (a `get purposes()`) is caller code that could mutate the sibling
|
|
2919
|
-
// map when read, so it is refused; an INHERITED map is refused too (own-only capture), never silently dropped,
|
|
2920
|
-
// so a restriction is never lost. A truly absent field is no restriction (undefined).
|
|
2921
1897
|
function _captureConstraintMap(entry, key, E, code, who) {
|
|
2922
1898
|
var d = intrinsic.getOwnPropertyDescriptor(entry, key);
|
|
2923
1899
|
if (d === undefined) {
|
|
@@ -2925,29 +1901,15 @@ function _captureConstraintMap(entry, key, E, code, who) {
|
|
|
2925
1901
|
return undefined;
|
|
2926
1902
|
}
|
|
2927
1903
|
if (!intrinsic.hasOwn(d, "value")) throw E(code, who + ": a trustAnchor " + key + " must be a data property, not an accessor");
|
|
2928
|
-
// A constraint map that is a Proxy is refused, the same way the top-level anchor is: its ownKeys / descriptor
|
|
2929
|
-
// traps could answer inconsistently -- an empty key list drops a restriction the caller attached, and an
|
|
2930
|
-
// isBuffer-answering trap could skip the snapshot entirely -- so the map cannot be trusted to describe itself.
|
|
2931
1904
|
if (intrinsic.types.isProxy(d.value)) throw E(code, who + ": a trustAnchor " + key + " map must not be a Proxy");
|
|
2932
1905
|
return d.value;
|
|
2933
1906
|
}
|
|
2934
1907
|
|
|
2935
|
-
// Read a field's OWN DATA value from its descriptor, WITHOUT invoking a getter -- an accessor or an inherited
|
|
2936
|
-
// field yields undefined. Used for the optional identity metadata (subjectDer / label / mozillaCaPolicy): a
|
|
2937
|
-
// getter there, invoked before the key is pinned, could overwrite the key Buffer or mutate the name, so it is
|
|
2938
|
-
// never invoked. This metadata does not enter a validation decision, so an accessor / inherited form is simply
|
|
2939
|
-
// not carried (unlike a constraint map, which is refused).
|
|
2940
1908
|
function _captureOwnData(entry, key) {
|
|
2941
1909
|
var d = intrinsic.getOwnPropertyDescriptor(entry, key);
|
|
2942
1910
|
return (d && intrinsic.hasOwn(d, "value")) ? d.value : undefined;
|
|
2943
1911
|
}
|
|
2944
1912
|
|
|
2945
|
-
// A fresh copy of a distinguished-name attribute record { type, value, name }, so the normalized anchor does
|
|
2946
|
-
// not share it with the caller. The consumed fields -- type + value drive the RFC 5280 sec. 7.1 dnEqual
|
|
2947
|
-
// comparison, name is the optional registry label -- are read by PROPERTY ACCESS, which follows the prototype
|
|
2948
|
-
// chain and reads an accessor once. The anchor API deliberately accepts an inherited / accessor name.rdns
|
|
2949
|
-
// (Object.create(base), a prototype getter), so an inherited attribute value must be SNAPSHOT here, not
|
|
2950
|
-
// dropped; a field the caller did not supply is omitted.
|
|
2951
1913
|
function _copyAtv(atv) {
|
|
2952
1914
|
var out = {};
|
|
2953
1915
|
var t = atv.type;
|
|
@@ -2959,33 +1921,18 @@ function _copyAtv(atv) {
|
|
|
2959
1921
|
return out;
|
|
2960
1922
|
}
|
|
2961
1923
|
|
|
2962
|
-
// DEEP-copy a caller's rdns so the normalized anchor's DN is independent of the caller's object. A single
|
|
2963
|
-
// arraySlice copies only the outer RDN sequence, leaving each nested RDN array and attribute record shared, so
|
|
2964
|
-
// a caller mutating name.rdns[i][j].value after normalization (or while an async validate awaits signature
|
|
2965
|
-
// verification) would change the issuer identity name chaining re-reads. Copy the outer array, each RDN array,
|
|
2966
|
-
// and each attribute record (its type/value are strings, so an own-data copy is enough) -- matching the
|
|
2967
|
-
// trust-store name copy (lib/trust.js _copyName).
|
|
2968
1924
|
function _copyRdnsDeep(rdns) {
|
|
2969
1925
|
if (!intrinsic.isArray(rdns)) return rdns;
|
|
2970
|
-
// Copy only OWN indexed elements and PRESERVE holes -- never read an inherited Array.prototype[i] slot. A
|
|
2971
|
-
// slice reads inherited indices (HasProperty walks the chain), so on a SPARSE rdns a polluted Array.prototype
|
|
2972
|
-
// slot would be materialized as an own element, filling a hole that guard.name.dnEqual explicitly rejects and
|
|
2973
|
-
// letting a malformed name pass chaining. Keeping the array sparse leaves the hole for the downstream guard.
|
|
2974
|
-
// Read own indices only (hasOwn) and CREATE each copied index with defineProperty, never a plain assignment:
|
|
2975
|
-
// both the read side (a slice walks the chain) and the write side (`out[i] = v` respects an inherited
|
|
2976
|
-
// non-writable data property or accessor -- it can throw, fail silently, or invoke an inherited setter) touch
|
|
2977
|
-
// the prototype, so a polluted Array.prototype could fill a hole or corrupt a dense rdns into a sparse one.
|
|
2978
|
-
// defineProperty is CreateDataProperty: it always makes an own data property regardless of the prototype.
|
|
2979
1926
|
var out = [];
|
|
2980
1927
|
out.length = rdns.length;
|
|
2981
1928
|
for (var i = 0; i < rdns.length; i++) {
|
|
2982
|
-
if (!intrinsic.hasOwn(rdns, i)) continue;
|
|
1929
|
+
if (!intrinsic.hasOwn(rdns, i)) continue;
|
|
2983
1930
|
var rdn = rdns[i], val;
|
|
2984
1931
|
if (intrinsic.isArray(rdn)) {
|
|
2985
1932
|
var rdnOut = [];
|
|
2986
1933
|
rdnOut.length = rdn.length;
|
|
2987
1934
|
for (var j = 0; j < rdn.length; j++) {
|
|
2988
|
-
if (!intrinsic.hasOwn(rdn, j)) continue;
|
|
1935
|
+
if (!intrinsic.hasOwn(rdn, j)) continue;
|
|
2989
1936
|
var atv = (rdn[j] !== null && typeof rdn[j] === "object") ? _copyAtv(rdn[j]) : rdn[j];
|
|
2990
1937
|
intrinsic.defineProperty(rdnOut, j, { value: atv, enumerable: true, configurable: true, writable: true });
|
|
2991
1938
|
}
|
|
@@ -2998,31 +1945,11 @@ function _copyRdnsDeep(rdns) {
|
|
|
2998
1945
|
|
|
2999
1946
|
function toAnchor(entry, verb) {
|
|
3000
1947
|
var who = verb || "build";
|
|
3001
|
-
// Decide tuple-vs-certificate by the discriminator fields, read under try/catch: a throwing name /
|
|
3002
|
-
// publicKey / algorithm accessor is a malformed anchor and must surface as the documented path/bad-input
|
|
3003
|
-
// entry refusal, not a raw exception leaked past the typed guards. `alg` and the raw publicKey are PINNED
|
|
3004
|
-
// to ONE read here and used throughout, so a stateful accessor that returns a value now and throws (or
|
|
3005
|
-
// returns a different value) on a later read cannot slip past this guard at the shape check or the snapshot.
|
|
3006
1948
|
var isTuple = false, alg = null, pubRaw, nameRaw = null, pk = null, purposesSnap, distrustSnap;
|
|
3007
1949
|
var subjectDerRaw, labelRaw, mozRaw;
|
|
3008
|
-
// A Proxy anchor is refused outright. Every field read below -- whether by property access or from an own
|
|
3009
|
-
// data descriptor -- is answered by the anchor's own reflection, and a Proxy's traps let the same read
|
|
3010
|
-
// return different values on different lookups or report a field absent while forwarding its siblings, so a
|
|
3011
|
-
// Proxy could hide a purposes / distrustAfter restriction the operator attached and validate against a
|
|
3012
|
-
// weaker anchor than the one supplied. A legitimate anchor is a plain tuple, a parsed certificate, or an
|
|
3013
|
-
// Object.create(base) inheriting from one -- none is a Proxy -- so refusing it here costs no real use and
|
|
3014
|
-
// removes the whole class of trap-driven anchor forgery. isProxy is an internal-slot check that runs no
|
|
3015
|
-
// caller code, so it sits outside the accessor try below and its message is not re-wrapped as an accessor throw.
|
|
3016
1950
|
if (intrinsic.types.isProxy(entry)) {
|
|
3017
1951
|
throw E("path/bad-input", who + ": a trustAnchor must not be a Proxy");
|
|
3018
1952
|
}
|
|
3019
|
-
// A Proxy anywhere in the anchor's prototype CHAIN is refused too. entry may legitimately inherit its fields
|
|
3020
|
-
// (an Object.create(base) anchor is supported), and those inherited reads -- the `in` tests and the property
|
|
3021
|
-
// accesses below -- walk the chain, so a Proxy prototype would run its has / get traps: the has trap could
|
|
3022
|
-
// report purposes / distrustAfter absent while the get trap still supplies inherited name / publicKey /
|
|
3023
|
-
// algorithm, dropping the restriction and validating a weaker anchor. Walk the chain refusing any Proxy,
|
|
3024
|
-
// checking isProxy BEFORE reading its prototype so a Proxy never runs a getPrototypeOf trap here. A legitimate
|
|
3025
|
-
// anchor -- a tuple, a certificate, or an Object.create(base) over a plain object -- has a Proxy-free chain.
|
|
3026
1953
|
if (entry !== null && typeof entry === "object") {
|
|
3027
1954
|
var proto = intrinsic.getPrototypeOf(entry);
|
|
3028
1955
|
while (proto !== null) {
|
|
@@ -3031,56 +1958,16 @@ function toAnchor(entry, verb) {
|
|
|
3031
1958
|
}
|
|
3032
1959
|
}
|
|
3033
1960
|
try {
|
|
3034
|
-
// Recognize a parsed CERTIFICATE first and route it to coerceCert below -- by PROVENANCE, not by reading
|
|
3035
|
-
// its fields. guard.parsed.isRecorded is a getter-free WeakMap lookup: a certificate from
|
|
3036
|
-
// pki.schema.x509.parse carries a provenance record, a hand-built tuple does not. A certificate does not
|
|
3037
|
-
// carry name / publicKey / algorithm as own properties, but the tuple discriminator reads those by
|
|
3038
|
-
// property access, which FOLLOWS the prototype chain -- so a polluted Object.prototype supplying those
|
|
3039
|
-
// three would otherwise misclassify a real certificate as a tuple and bind the INHERITED (attacker) key
|
|
3040
|
-
// instead of the certificate's own. Recognizing the certificate by its forge-proof provenance mark routes
|
|
3041
|
-
// it to coerceCert and never binds an inherited key. Crucially the check is a WeakMap lookup that invokes
|
|
3042
|
-
// NO caller getter -- so, unlike a structural probe that would read tbsBytes / subject / ... , it cannot run
|
|
3043
|
-
// a tuple-controlled accessor (e.g. a polluted Object.prototype.tbsBytes getter) before publicKey is pinned
|
|
3044
|
-
// below, which would otherwise defeat the key-pinning guarantee.
|
|
3045
1961
|
if (entry && typeof entry === "object" && !Buffer.isBuffer(entry) && !guard.parsed.isRecorded(entry)) {
|
|
3046
|
-
// Not a recorded certificate (isRecorded is a getter-free WeakMap lookup), so treat entry as a tuple
|
|
3047
|
-
// candidate. entry is not a Proxy (refused at the door above), so its own reflection is truthful: a
|
|
3048
|
-
// getOwnPropertyDescriptor read reports its real own data, and an absent field is really absent rather
|
|
3049
|
-
// than a trap hiding a forwarded one. The discriminator fields (publicKey / name / algorithm) are read
|
|
3050
|
-
// by PROPERTY ACCESS (follows the prototype chain, so an Object.create(base) inherited anchor is
|
|
3051
|
-
// recognized), each exactly once, so a stateful own accessor answers each at most once. The prototype
|
|
3052
|
-
// chain is never WALKED (Buffer.isBuffer / isRecorded above invoke getPrototypeOf a bounded number of
|
|
3053
|
-
// times only), so a hostile getPrototypeOf cannot be driven into a loop.
|
|
3054
|
-
//
|
|
3055
|
-
// Snapshot the constraint maps FIRST -- before ANY discriminator field (publicKey / name / algorithm) is
|
|
3056
|
-
// read -- so no discriminator getter can flip a denied purpose to allowed or move a distrust cutoff
|
|
3057
|
-
// between the map's capture and validation. A getter for publicKey or name runs when that field is read;
|
|
3058
|
-
// capturing purposes / distrustAfter beforehand puts them out of its reach (the same protection the
|
|
3059
|
-
// ordering already gave against the later algorithm getter, now extended to the earlier ones). The
|
|
3060
|
-
// identity passthrough (subjectDer, label, mozillaCaPolicy -- the closed producer-derived set) is read
|
|
3061
|
-
// once each here too. entry is not a recorded certificate, so these reads do not touch a cert's accessors.
|
|
3062
1962
|
var pRaw = _captureConstraintMap(entry, "purposes", E, "path/bad-input", who);
|
|
3063
1963
|
purposesSnap = _normalizeConstraintField(pRaw, E, "path/bad-input", who);
|
|
3064
1964
|
var dRaw = _captureConstraintMap(entry, "distrustAfter", E, "path/bad-input", who);
|
|
3065
1965
|
distrustSnap = _normalizeConstraintField(dRaw, E, "path/bad-input", who);
|
|
3066
|
-
// The identity passthrough (subjectDer, label, mozillaCaPolicy) is captured from its OWN DATA descriptor
|
|
3067
|
-
// too, never by property access: an accessor identity field, read before the key is pinned below, could
|
|
3068
|
-
// overwrite the key Buffer or mutate the name, so its getter is never invoked. An accessor / inherited
|
|
3069
|
-
// identity field is not carried (it is non-validation metadata), not refused.
|
|
3070
1966
|
subjectDerRaw = _captureOwnData(entry, "subjectDer");
|
|
3071
1967
|
labelRaw = _captureOwnData(entry, "label");
|
|
3072
1968
|
mozRaw = _captureOwnData(entry, "mozillaCaPolicy");
|
|
3073
|
-
// Every mutable, validation-relevant field above (the constraint maps and the identity metadata) was
|
|
3074
|
-
// captured from a descriptor WITHOUT invoking a getter, so the publicKey read below -- and the name /
|
|
3075
|
-
// algorithm reads after it -- cannot have mutated them. publicKey is read (property access, so an
|
|
3076
|
-
// inherited publicKey is supported) and pinned to a private Buffer IMMEDIATELY, before the name /
|
|
3077
|
-
// algorithm accessors run, so a later getter cannot overwrite the key Buffer after it is read but before
|
|
3078
|
-
// it is captured. A non-tuple leaves pk null / isTuple false and falls through to coerceCert below.
|
|
3079
1969
|
pubRaw = entry.publicKey;
|
|
3080
1970
|
if (guard.bytes.isByteSource(pubRaw)) {
|
|
3081
|
-
// snapshotSource, not snapshot: isByteSource accepts all four BufferSource forms (Buffer, any view,
|
|
3082
|
-
// DataView, ArrayBuffer), so the normalizer must too -- snapshot accepts only Buffer/Uint8Array and
|
|
3083
|
-
// would accept-then-throw on an ArrayBuffer / DataView anchor publicKey.
|
|
3084
1971
|
pk = guard.bytes.snapshotSource(pubRaw, PathError, "path/bad-input", who + ": trustAnchor publicKey");
|
|
3085
1972
|
}
|
|
3086
1973
|
nameRaw = entry.name;
|
|
@@ -3091,38 +1978,6 @@ function toAnchor(entry, verb) {
|
|
|
3091
1978
|
throw E("path/bad-input", who + ": a trustAnchor accessor threw", e);
|
|
3092
1979
|
}
|
|
3093
1980
|
if (isTuple) {
|
|
3094
|
-
// publicKey was read FIRST and pinned to `pk` in the discriminator above, before the algorithm and name
|
|
3095
|
-
// accessors ran, so no caller getter can have mutated the key Buffer between its read and its capture.
|
|
3096
|
-
// (A non-Buffer publicKey leaves pk null and is refused by the Buffer.isBuffer shape check below.)
|
|
3097
|
-
// A ready anchor tuple: validate the shape build + validate consume -- name.rdns
|
|
3098
|
-
// (name matching), publicKey bytes and the algorithm OID (the sec. 6.1.4 key
|
|
3099
|
-
// hand-off). trustAnchors is a caller option, so a malformed tuple is a config
|
|
3100
|
-
// error and fails closed at entry, not a downstream no-path / soft verdict.
|
|
3101
|
-
// `algorithm` is the anchor key's own algorithm: an OID string, or the SubjectPublicKeyInfo
|
|
3102
|
-
// `algorithm` object a parsed certificate carries ({ oid, parameters }), which validate has always
|
|
3103
|
-
// accepted. Built-in verification derives the key algorithm from the (valid) publicKey SPKI and never
|
|
3104
|
-
// consults this value, so any algorithm that slips through -- an object with no `oid`, or an oid that
|
|
3105
|
-
// is not a real OID -- would validate the path on the SPKI alone and leave the very fail-open this
|
|
3106
|
-
// check exists to close. A number, an object without an oid, or another non-shape is a malformed tuple
|
|
3107
|
-
// and fails closed; a MISSING algorithm skips this branch and is caught below as not-a-certificate.
|
|
3108
|
-
// Materialize the anchor name to a fresh plain object carrying only its CONSUMED sub-fields: rdns (used
|
|
3109
|
-
// for DN matching -- read through the chain, so an inherited / Object.create(base) rdns is kept, then
|
|
3110
|
-
// DEEP-copied (the outer array, each RDN array, and each attribute record) so a later mutation of the
|
|
3111
|
-
// caller's nested RDN cannot change the issuer DN name chaining re-reads) and bytes (the DN DER a
|
|
3112
|
-
// cert-derived name carries, kept for a lossless round-trip; not consumed in a name-matching decision, so
|
|
3113
|
-
// it is held by reference). Each is read ONCE as DATA; the name object's own shape is never enumerated. path
|
|
3114
|
-
// validation re-reads
|
|
3115
|
-
// workingIssuerName.rdns during name chaining, so a stateful rdns accessor that answered the shape check
|
|
3116
|
-
// with one array must not hand chaining a different issuer DN; the single read stores that one value. A
|
|
3117
|
-
// throwing name / name.rdns accessor is a malformed anchor -> path/bad-input, not a raw throw.
|
|
3118
|
-
// Capture the two NESTED tuple fields -- algorithm.oid and name.rdns / name.bytes -- GETTER-FREE, from their
|
|
3119
|
-
// own data descriptors, never by property access. An ACCESSOR nested field runs caller code that could mutate
|
|
3120
|
-
// the sibling before it is captured: a name.rdns getter rewriting a wrong algorithm.oid to the SPKI's real one
|
|
3121
|
-
// past the mismatch refusal, or an algorithm.oid getter rewriting the anchor DN. Because BOTH directions
|
|
3122
|
-
// cross-mutate, no read ORDER is safe -- so an accessor nested field is refused and no getter ever runs. A
|
|
3123
|
-
// Proxy-hosted algorithm / name object is refused before its traps can answer. A legitimate anchor -- a tuple
|
|
3124
|
-
// with a plain { oid } algorithm and a plain { rdns, bytes } name, or a parsed certificate's own -- carries
|
|
3125
|
-
// plain data and is unaffected.
|
|
3126
1981
|
var algStr;
|
|
3127
1982
|
try {
|
|
3128
1983
|
if (typeof alg === "string") {
|
|
@@ -3153,12 +2008,6 @@ function toAnchor(entry, verb) {
|
|
|
3153
2008
|
if (nBytes !== undefined) {
|
|
3154
2009
|
intrinsic.defineProperty(nameOut, "bytes", { value: nBytes, enumerable: true, configurable: true, writable: true });
|
|
3155
2010
|
}
|
|
3156
|
-
// Preserve the parsed Name's dn (the rendered "CN=..., O=..." string a parser emits, used to display /
|
|
3157
|
-
// identify the selected root), so a pki.trust anchor or a cert-derived name round-trips its full shape --
|
|
3158
|
-
// { rdns, dn, bytes } -- through normalization. Only a STRING dn is carried: identity never flows through
|
|
3159
|
-
// this display string (name chaining compares rdns / bytes DER under RFC 5280 sec. 7.1), so an object- or
|
|
3160
|
-
// otherwise non-string-valued dn -- which could run coercion code in a display consumer -- is dropped, not
|
|
3161
|
-
// carried. Captured getter-free like the others (an accessor dn could otherwise mutate a sibling).
|
|
3162
2011
|
var dnD = nsrc ? intrinsic.getOwnPropertyDescriptor(nsrc, "dn") : undefined;
|
|
3163
2012
|
if (dnD && !intrinsic.hasOwn(dnD, "value")) throw E("path/bad-input", who + ": a trustAnchor name.dn must be a data property, not an accessor");
|
|
3164
2013
|
var nDn = dnD ? dnD.value : undefined;
|
|
@@ -3166,58 +2015,17 @@ function toAnchor(entry, verb) {
|
|
|
3166
2015
|
intrinsic.defineProperty(nameOut, "dn", { value: nDn, enumerable: true, configurable: true, writable: true });
|
|
3167
2016
|
}
|
|
3168
2017
|
} catch (e) { throw E("path/bad-input", who + ": trustAnchor name could not be materialized", e); }
|
|
3169
|
-
// hasOwn-gate the read: nameOut is materialized here, so its rdns is either an OWN data property (set above
|
|
3170
|
-
// from the caller's own rdns) or absent. Testing the OWN property -- not a plain nameOut.rdns, which would
|
|
3171
|
-
// resolve a polluted Object.prototype.rdns on a name that supplied none (and could invoke an inherited
|
|
3172
|
-
// throwing getter) -- means a name with no own rdns is refused, never accepted on an inherited array.
|
|
3173
2018
|
var nameHasRdns = intrinsic.hasOwn(nameOut, "rdns") && intrinsic.isArray(nameOut.rdns);
|
|
3174
|
-
// Gate on the NORMALIZED key `pk` (a private Buffer from snapshotSource iff publicKey was any of the four
|
|
3175
|
-
// BufferSource forms), not the raw `pubRaw` -- Buffer.isBuffer(pubRaw) would reject a Uint8Array / view /
|
|
3176
|
-
// ArrayBuffer publicKey that isByteSource + snapshotSource just accepted.
|
|
3177
2019
|
if (!nameHasRdns || !Buffer.isBuffer(pk) || algStr === null) {
|
|
3178
2020
|
throw E("path/bad-input", who + ": a trustAnchor tuple must be { name: { rdns: [...] }, publicKey: Buffer, algorithm: an OID string or a SubjectPublicKeyInfo algorithm carrying an oid }");
|
|
3179
2021
|
}
|
|
3180
|
-
// Hold the stated algorithm to a canonical dotted-decimal OID: a shape-valid but non-canonical value
|
|
3181
|
-
// ("not-an-oid", { oid: "garbage" }) would otherwise pass and never match the SPKI-derived algorithm.
|
|
3182
2022
|
guard.identifier.assertCanonicalOid(algStr, E, "path/bad-input", who + ": trustAnchor algorithm " + algStr);
|
|
3183
|
-
// The declared algorithm is redundant with the self-describing publicKey SPKI, and built-in verification
|
|
3184
|
-
// reads the SPKI's own key algorithm, never this field. A canonical but WRONG OID (declaring one key
|
|
3185
|
-
// algorithm for a key that is actually another) would otherwise be accepted and the path validate on the
|
|
3186
|
-
// SPKI alone. Reject a declared OID that does not equal the SubjectPublicKeyInfo's key-algorithm OID.
|
|
3187
|
-
// (publicKey was pinned to `pk` at the top of this branch, before any caller getter could mutate it.)
|
|
3188
|
-
//
|
|
3189
|
-
// Walk the whole SubjectPublicKeyInfo through the shared PKIX schema, not just its first OID: a structure
|
|
3190
|
-
// that carries a decodable AlgorithmIdentifier but no key BIT STRING would otherwise pass this gate and
|
|
3191
|
-
// slip to a soft valid:false at key-import time, bypassing the promised path/bad-input entry refusal.
|
|
3192
2023
|
var spkiAlg;
|
|
3193
2024
|
try { spkiAlg = schema.walk(ANCHOR_SPKI_SCHEMA, asn1.decode(pk), NS).result.algorithm; }
|
|
3194
2025
|
catch (e) { throw E("path/bad-input", who + ": trustAnchor publicKey is not a valid SubjectPublicKeyInfo", e); }
|
|
3195
2026
|
if (spkiAlg.oid !== algStr) {
|
|
3196
2027
|
throw E("path/bad-input", who + ": trustAnchor algorithm " + algStr + " does not match its publicKey key algorithm " + spkiAlg.oid);
|
|
3197
2028
|
}
|
|
3198
|
-
// Return a SELF-CONTAINED anchor: a fresh plain object carrying ONLY the closed, producer-derived anchor
|
|
3199
|
-
// field set -- name, publicKey, algorithm, parameters, purposes, distrustAfter, subjectDer, label,
|
|
3200
|
-
// mozillaCaPolicy -- the shape pki.trust.parseCertdata documents and path validation + trust dedup
|
|
3201
|
-
// consume. Every field was read exactly once above (or is derived from the validated SPKI / pinned key),
|
|
3202
|
-
// so a caller getter or Proxy trap runs at most once per field and no second read can disagree with the
|
|
3203
|
-
// first; the anchor's own shape is NEVER enumerated (no ownKeys / getOwnPropertyDescriptor / `in`) and the
|
|
3204
|
-
// prototype chain is never walked, so a Proxy cannot use an enumeration trap to mutate state and a hostile
|
|
3205
|
-
// getPrototypeOf cannot be driven into a loop (Buffer.isBuffer invokes it at most once). A field the caller
|
|
3206
|
-
// attached OUTSIDE this set is not carried -- an anchor has a DEFINED shape, not an arbitrary passthrough --
|
|
3207
|
-
// and `__proto__` is never assigned, so no caller field reaches Object.prototype's setter.
|
|
3208
|
-
// - name, purposes, distrustAfter: materialized above from ONE read each as DATA (an inherited
|
|
3209
|
-
// restriction is kept, since those reads followed the prototype chain). Storing DATA is load-bearing --
|
|
3210
|
-
// assertAnchorConstraints reads distrustAfter several times (a truthy gate, a hasOwn gate, then the
|
|
3211
|
-
// value) and _hasPurposeScopedMetadata / the purpose block read purposes at more than one site, so a
|
|
3212
|
-
// live accessor could answer those reads inconsistently and silently drop a distrust or purpose control.
|
|
3213
|
-
// - subjectDer, label, mozillaCaPolicy: the trust-store identity fields, read once each in the
|
|
3214
|
-
// discriminator, so a store's own entry round-trips onto build's result.trustAnchor.
|
|
3215
|
-
// - publicKey is the pinned copy; algorithm and parameters come from the validated SPKI, never a
|
|
3216
|
-
// caller-declared field. Seeding the working key from a string OID keeps the sec. 6.1.4 key-algorithm
|
|
3217
|
-
// comparison working (an object never equals a cert's keyAlg.oid); taking parameters from the key's own
|
|
3218
|
-
// AlgorithmIdentifier stops a declared value that disagrees with it (e.g. P-384 params on a P-256
|
|
3219
|
-
// anchor) from being promoted into workingPublicKeyParameters and inherited by a child that omits its
|
|
3220
|
-
// own, which would validate a chain RFC 5280 inheritance from the real params rejects.
|
|
3221
2029
|
var flat = {};
|
|
3222
2030
|
intrinsic.defineProperty(flat, "name", { value: nameOut, enumerable: true, configurable: true, writable: true });
|
|
3223
2031
|
if (purposesSnap !== undefined) intrinsic.defineProperty(flat, "purposes", { value: purposesSnap, enumerable: true, configurable: true, writable: true });
|
|
@@ -3230,14 +2038,6 @@ function toAnchor(entry, verb) {
|
|
|
3230
2038
|
intrinsic.defineProperty(flat, "parameters", { value: spkiAlg.parameters, enumerable: true, configurable: true, writable: true });
|
|
3231
2039
|
return flat;
|
|
3232
2040
|
}
|
|
3233
|
-
// A certificate anchor carries no per-purpose constraints: the certificate branch below returns { name,
|
|
3234
|
-
// publicKey, algorithm, ... } only. Per-purpose purposes / distrustAfter come from a trust-anchor TUPLE (what
|
|
3235
|
-
// pki.trust emits), never from properties attached to a parsed certificate -- attaching them there would
|
|
3236
|
-
// silently drop the restriction and validate a path the caller meant to forbid. Refuse rather than drop. The
|
|
3237
|
-
// `in` check catches an OWN or an INHERITED purposes / distrustAfter (a name reached through the prototype, e.g.
|
|
3238
|
-
// a polluted Object.prototype), matching how the tuple branch refuses an inherited constraint field; it invokes
|
|
3239
|
-
// no getter (an existence test), and entry is not a Proxy (refused at the door), so no trap runs. A normal
|
|
3240
|
-
// certificate has neither on its chain, so this only catches the footgun of hanging a constraint on a cert.
|
|
3241
2041
|
if (entry && typeof entry === "object" && (("purposes" in entry) || ("distrustAfter" in entry))) {
|
|
3242
2042
|
throw E("path/bad-input", who + ": a trustAnchor certificate must not carry purposes / distrustAfter -- put per-purpose constraints on a { name, publicKey, algorithm, purposes, distrustAfter } tuple (or a pki.trust anchor), not on a parsed certificate");
|
|
3243
2043
|
}
|
|
@@ -3270,20 +2070,12 @@ function toAnchor(entry, verb) {
|
|
|
3270
2070
|
*/
|
|
3271
2071
|
function anchorFromCert(cert) { return toAnchor(cert, "anchorFromCert"); }
|
|
3272
2072
|
|
|
3273
|
-
// RFC 4158 sec. 2.4.2 loop key: the (subject DN + subjectAltName + subject
|
|
3274
|
-
// public key) tuple, NOT the DN alone. DN-alone keying wrongly prunes a
|
|
3275
|
-
// legitimate distinct cross-cert (cross-certs share a DN) and misses a same-key
|
|
3276
|
-
// loop; the key differentiates a rollover (same DN, new key) from a true repeat.
|
|
3277
2073
|
function identityKey(cert) {
|
|
3278
2074
|
var san = findExt(cert, OID.subjectAltName);
|
|
3279
2075
|
return cert.subject.bytes.toString("hex") + "|" + (san ? san.value.toString("hex") : "") + "|" +
|
|
3280
2076
|
cert.subjectPublicKeyInfo.bytes.toString("hex");
|
|
3281
2077
|
}
|
|
3282
2078
|
|
|
3283
|
-
// A byte-EXACT certificate identity (the signed tbs region + the signature), for deduping certificates fetched
|
|
3284
|
-
// over AIA before they enter the shared pool. Unlike identityKey (a subject/SAN/key tuple, deliberately broad for
|
|
3285
|
-
// loop pruning), this collapses ONLY true byte-duplicates -- so a mirror URL or a repeating CMS returning the same
|
|
3286
|
-
// issuer is added once, but a functionally-different cert sharing a subject+key (e.g. a key rollover) is kept.
|
|
3287
2079
|
function certDerKey(cert) {
|
|
3288
2080
|
return cert.tbsBytes.toString("base64") + "|" + (cert.signatureValue && cert.signatureValue.bytes ? cert.signatureValue.bytes.toString("base64") : "");
|
|
3289
2081
|
}
|
|
@@ -3293,47 +2085,34 @@ function childAkiKeyId(cert) {
|
|
|
3293
2085
|
return (d && d.value && d.value.keyIdentifier) ? d.value.keyIdentifier : null;
|
|
3294
2086
|
}
|
|
3295
2087
|
|
|
3296
|
-
// RFC 4158 sec. 3.5 candidate prioritization -- a SORT weight, never a gate.
|
|
3297
|
-
// Higher = tried first (fewer validate calls to reach a valid path). Every term
|
|
3298
|
-
// is a hint: a KID match, an anchor-adjacent issuer, CA + keyCertSign presence,
|
|
3299
|
-
// and validity at the check time all raise priority, but a lower-scoring
|
|
3300
|
-
// candidate is STILL attempted, and validate makes every accept decision.
|
|
3301
2088
|
function scoreCandidate(cand, childAki, anchors, time) {
|
|
3302
2089
|
var score = 0;
|
|
3303
2090
|
if (childAki) {
|
|
3304
2091
|
var d = softDecode(cand, OID.subjectKeyIdentifier);
|
|
3305
|
-
if (d && Buffer.isBuffer(d.value) && d.value.equals(childAki)) score += 1000;
|
|
2092
|
+
if (d && Buffer.isBuffer(d.value) && d.value.equals(childAki)) score += 1000;
|
|
3306
2093
|
}
|
|
3307
2094
|
for (var i = 0; i < anchors.length; i++) {
|
|
3308
|
-
if (nameMatchSoft(cand.issuer.rdns, anchors[i].name.rdns)) { score += 100; break; }
|
|
2095
|
+
if (nameMatchSoft(cand.issuer.rdns, anchors[i].name.rdns)) { score += 100; break; }
|
|
3309
2096
|
}
|
|
3310
2097
|
var bc = softDecode(cand, OID.basicConstraints);
|
|
3311
|
-
if (bc && bc.value && bc.value.cA === true) score += 10;
|
|
2098
|
+
if (bc && bc.value && bc.value.cA === true) score += 10;
|
|
3312
2099
|
var ku = softDecode(cand, OID.keyUsage);
|
|
3313
|
-
if (ku && ku.value && ku.value.keyCertSign === true) score += 10;
|
|
2100
|
+
if (ku && ku.value && ku.value.keyCertSign === true) score += 10;
|
|
3314
2101
|
var v = cand.validity;
|
|
3315
2102
|
// allow:nan-date-comparison-unguarded -- cand.validity dates are codec-parsed (asn1 readTime rejects a NaN instant) and time is guard.time.assertValid'd at entry; the validity term is a fail-safe sort hint regardless.
|
|
3316
2103
|
if (v && guard.time.isDate(v.notBefore) && guard.time.isDate(v.notAfter) &&
|
|
3317
2104
|
guard.time.instantOf(v.notBefore) <= guard.time.instantOf(time) &&
|
|
3318
|
-
guard.time.instantOf(time) <= guard.time.instantOf(v.notAfter)) score += 5;
|
|
2105
|
+
guard.time.instantOf(time) <= guard.time.instantOf(v.notAfter)) score += 5;
|
|
3319
2106
|
return score;
|
|
3320
2107
|
}
|
|
3321
2108
|
|
|
3322
|
-
// Sort scored issuer candidates by descending priority and push each non-looping one onto the DFS stack as a
|
|
3323
|
-
// child frame (the chain grows leaf-ward by PREPENDING the issuer). Shared by the static-pool expansion and the
|
|
3324
|
-
// AIA-fallback expansion so both enforce the SAME total-work counter (sec. 3.5 breadth cap), loop pruning
|
|
3325
|
-
// (the identity visited-set), and priority order. `scored` is [{cand, score}]; returns the number of candidates
|
|
3326
|
-
// ticked so the caller can advance its `considered` tally.
|
|
3327
2109
|
function _pushCandidates(frame, scored, stack, counter) {
|
|
3328
|
-
scored.sort(function (a, b) { return a.score - b.score; });
|
|
2110
|
+
scored.sort(function (a, b) { return a.score - b.score; });
|
|
3329
2111
|
var n = 0;
|
|
3330
2112
|
for (var ci = 0; ci < scored.length; ci++) {
|
|
3331
|
-
counter.tick();
|
|
2113
|
+
counter.tick();
|
|
3332
2114
|
n += 1;
|
|
3333
2115
|
var cand = scored[ci].cand, candKey = identityKey(cand);
|
|
3334
|
-
// Loop detection and the dedupe sets below are resource bounds, so they are asked through the
|
|
3335
|
-
// captured methods: a replaced `Set.prototype.has` answering false never prunes and the build
|
|
3336
|
-
// walks a cycle until a cap stops it.
|
|
3337
2116
|
if (intrinsic.setHas(frame.keys, candKey)) continue;
|
|
3338
2117
|
var childKeys = new Set(frame.keys);
|
|
3339
2118
|
intrinsic.setAdd(childKeys, candKey);
|
|
@@ -3342,112 +2121,69 @@ function _pushCandidates(frame, scored, stack, counter) {
|
|
|
3342
2121
|
return n;
|
|
3343
2122
|
}
|
|
3344
2123
|
|
|
3345
|
-
|
|
3346
|
-
// Discover a MISSING intermediate by GETting the caIssuers accessLocation of the certificate being chained
|
|
3347
|
-
// past, feeding the fetched cert(s) into the SAME candidate search. SSRF / amplification bounded: https-only,
|
|
3348
|
-
// NO private / loopback / link-local destination -- an IP LITERAL is refused by the pre-check, and a DNS NAME
|
|
3349
|
-
// that RESOLVES to such an address is refused (and the address pinned) by the transport's blockPrivateAddresses
|
|
3350
|
-
// filter set on every AIA request, so an untrusted cert cannot drive an authenticated GET to an internal service
|
|
3351
|
-
// / cloud metadata by literal OR by hostname; a
|
|
3352
|
-
// total fetch budget (a SILENT cap -- stop fetching, never a throw that aborts a buildable path), a per-cert URL
|
|
3353
|
-
// cap over DISTINCT normalized URLs, a build-wide URL dedupe (fragment-free), a response size + certificate-count
|
|
3354
|
-
// cap, no redirect following. Every fetch fault is a SKIP (the DFS continues over the pool). A fetched cert is UNTRUSTED pool
|
|
3355
|
-
// material -- when validate is on (the default) it flows through validate() like any candidate and is NEVER a
|
|
3356
|
-
// trust anchor; in pure-builder mode (opts.validate:false) it is returned unvalidated, exactly like a static candidate.
|
|
3357
|
-
|
|
3358
|
-
// SSRF guard (literal pre-check): is a URL host a private / loopback / link-local / reserved IP LITERAL? An AIA
|
|
3359
|
-
// URL comes from an UNTRUSTED certificate, so a literal address into RFC 1918 / loopback / the 169.254 cloud-
|
|
3360
|
-
// metadata range must not be fetched (with enterprise TLS trust it would be an authenticated GET to an internal
|
|
3361
|
-
// service). This is a fast pre-filter that avoids even opening a socket for an obvious literal; a DNS NAME is
|
|
3362
|
-
// judged at RESOLUTION time by the transport's blockPrivateAddresses filter (set on the AIA request below), which
|
|
3363
|
-
// also pins the checked address. The IP classification is shared with the transport (one range set, no drift).
|
|
2124
|
+
|
|
3364
2125
|
function _isBlockedAiaHost(host) {
|
|
3365
|
-
if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1);
|
|
3366
|
-
if (net.isIP(host) === 0) return false;
|
|
3367
|
-
return httpTransport.isBlockedIp(host);
|
|
2126
|
+
if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1);
|
|
2127
|
+
if (net.isIP(host) === 0) return false;
|
|
2128
|
+
return httpTransport.isBlockedIp(host);
|
|
3368
2129
|
}
|
|
3369
2130
|
|
|
3370
|
-
// Parse an AIA response body as a single DER certificate (RFC 2585) OR a certs-only CMS bundle (RFC 5272).
|
|
3371
|
-
// The media type is only an ordering HINT (RFC 5280 sec. 4.2.2.1: "should not depend solely on the ... media
|
|
3372
|
-
// type") -- both structures are attempted, the wire decides. Returns raw certificate DER Buffers; throws
|
|
3373
|
-
// (caught upstream as a skip) if the body is neither.
|
|
3374
2131
|
function _aiaParseBody(body, contentType, maxCerts) {
|
|
3375
|
-
var certsFirst = String(contentType || "").toLowerCase().indexOf("pkcs7") >= 0;
|
|
2132
|
+
var certsFirst = String(contentType || "").toLowerCase().indexOf("pkcs7") >= 0;
|
|
3376
2133
|
var order = certsFirst ? ["certs", "cert"] : ["cert", "certs"];
|
|
3377
2134
|
for (var i = 0; i < order.length; i++) {
|
|
3378
2135
|
try {
|
|
3379
2136
|
if (order[i] === "cert") { x509.parse(body); return [Buffer.from(body)]; }
|
|
3380
|
-
return cms.parseCertsOnly(body, E, "path", maxCerts).certificates;
|
|
3381
|
-
} catch (_e) {
|
|
2137
|
+
return cms.parseCertsOnly(body, E, "path", maxCerts).certificates;
|
|
2138
|
+
} catch (_e) { }
|
|
3382
2139
|
}
|
|
3383
2140
|
throw E("path/aia-bad-body", "an AIA response body is neither a DER certificate nor a certs-only CMS");
|
|
3384
2141
|
}
|
|
3385
2142
|
|
|
3386
|
-
// Fetch ONE caIssuers URL over the injected/default transport; returns the parsed candidate certs, or throws
|
|
3387
|
-
// (the caller collapses any throw to a skip). Only a 200 with a non-empty, in-cap body is a cert source (M12).
|
|
3388
2143
|
async function _aiaFetchOne(uri, aia) {
|
|
3389
|
-
// blockPrivateAddresses: the real transport refuses -- and pins -- a hostname that RESOLVES to a private /
|
|
3390
|
-
// loopback / link-local address (the literal pre-check only catches an IP literal). An injected test transport
|
|
3391
|
-
// ignores the flag; the DFS treats a blocked-address transport error as a silent skip like any fetch fault.
|
|
3392
2144
|
var res = await aia.transport({ method: "GET", url: uri, tls: aia.tls, timeout: aia.timeout, maxResponseBytes: aia.maxResponseBytes, blockPrivateAddresses: true });
|
|
3393
2145
|
res = res || {};
|
|
3394
2146
|
if (res.status !== 200) throw E("path/aia-status", "an AIA fetch returned HTTP " + res.status + " (only 200 is a cert source; no redirect following)");
|
|
3395
|
-
// A binary view from an injected transport (Buffer / Uint8Array / ArrayBuffer / DataView) is re-viewed
|
|
3396
|
-
// through the byte guard, as the request path does; only a genuine string body falls back to a latin1
|
|
3397
|
-
// decode -- a typed array is never String()-coerced to "48,130,..." garbage that then fails to parse.
|
|
3398
2147
|
var body = guard.bytes.isByteSource(res.body)
|
|
3399
2148
|
? guard.bytes.source(res.body, PathError, "path/aia-bad-body", "the AIA response body")
|
|
3400
2149
|
: Buffer.from(res.body == null ? "" : String(res.body), "latin1");
|
|
3401
2150
|
if (body.length === 0) throw E("path/aia-empty", "an AIA fetch returned an empty body");
|
|
3402
|
-
// Belt for an INJECTED transport that ignores maxResponseBytes (the real transport streaming-aborts at the cap).
|
|
3403
2151
|
if (body.length > aia.maxResponseBytes) throw E("path/aia-too-large", "an AIA response exceeds the " + aia.maxResponseBytes + "-byte cap");
|
|
3404
2152
|
var headers = {};
|
|
3405
2153
|
Object.keys(res.headers || {}).forEach(function (k) { headers[k.toLowerCase()] = res.headers[k]; });
|
|
3406
2154
|
return _aiaParseBody(body, headers["content-type"], aia.maxCertsPerResponse);
|
|
3407
2155
|
}
|
|
3408
2156
|
|
|
3409
|
-
// Discover issuer candidates for `current` from its AIA caIssuers URLs. Returns parsed candidate certs (coerced
|
|
3410
|
-
// like any pool cert). Bounded + fail-closed; ticks aia.fetchCounter (the total-budget throw) and mutates
|
|
3411
|
-
// aia.fetchedUrls (build-wide dedupe). A cert with no AIA, a malformed AIA, or no fetchable https caIssuers
|
|
3412
|
-
// URI simply yields no candidates (RFC 5280 sec. 4.2.2.1 AIA is advisory / non-critical).
|
|
3413
2157
|
async function _fetchAiaIssuers(current, aia) {
|
|
3414
|
-
var d = softDecode(current, OID.authorityInfoAccess);
|
|
2158
|
+
var d = softDecode(current, OID.authorityInfoAccess);
|
|
3415
2159
|
if (!d || !d.value) return [];
|
|
3416
|
-
// Collect DISTINCT, normalized, fetchable https URLs, deduping BEFORE the per-cert cap so a flood of duplicate
|
|
3417
|
-
// (or fragment-variant) entries can never crowd out a usable later URL.
|
|
3418
2160
|
var uris = [];
|
|
3419
2161
|
var seenThisCert = new Set();
|
|
3420
2162
|
for (var i = 0; i < d.value.length; i++) {
|
|
3421
2163
|
var ad = d.value[i];
|
|
3422
|
-
if (ad.accessMethod !== OID.caIssuers) continue;
|
|
3423
|
-
if (!ad.accessLocation || ad.accessLocation.tag !== 6) continue;
|
|
3424
|
-
// Parse the URI ONCE: the NORMALIZED href is both the dedupe key AND the exact URL handed to the transport.
|
|
2164
|
+
if (ad.accessMethod !== OID.caIssuers) continue;
|
|
2165
|
+
if (!ad.accessLocation || ad.accessLocation.tag !== 6) continue;
|
|
3425
2166
|
var u;
|
|
3426
2167
|
try { u = new URL(ad.accessLocation.value); }
|
|
3427
|
-
catch (_e) { continue; }
|
|
3428
|
-
if (u.protocol !== "https:") continue;
|
|
3429
|
-
if (_isBlockedAiaHost(u.hostname)) continue;
|
|
3430
|
-
// A DNS-NAME host's private RESOLUTION can only be blocked by a transport that filters the resolved address;
|
|
3431
|
-
// with an UNGUARDED transport (an injected fn that does not vouch blocksPrivateAddresses) fail closed -- fetch
|
|
3432
|
-
// IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
|
|
2168
|
+
catch (_e) { continue; }
|
|
2169
|
+
if (u.protocol !== "https:") continue;
|
|
2170
|
+
if (_isBlockedAiaHost(u.hostname)) continue;
|
|
3433
2171
|
if (net.isIP(_stripBrackets(u.hostname)) === 0 && !aia.transportGuardsAddresses) continue;
|
|
3434
|
-
u.hash = "";
|
|
3435
|
-
if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue;
|
|
3436
|
-
if (seenThisCert.size >= aia.maxPerCert) break;
|
|
2172
|
+
u.hash = "";
|
|
2173
|
+
if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue;
|
|
2174
|
+
if (seenThisCert.size >= aia.maxPerCert) break;
|
|
3437
2175
|
intrinsic.setAdd(seenThisCert, u.href);
|
|
3438
2176
|
uris.push(u.href);
|
|
3439
2177
|
}
|
|
3440
2178
|
var out = [];
|
|
3441
2179
|
for (var k = 0; k < uris.length; k++) {
|
|
3442
|
-
if (intrinsic.setHas(aia.fetchedUrls, uris[k])) continue;
|
|
3443
|
-
if (aia.fetches >= aia.maxFetches) break;
|
|
3444
|
-
intrinsic.setAdd(aia.fetchedUrls, uris[k]);
|
|
2180
|
+
if (intrinsic.setHas(aia.fetchedUrls, uris[k])) continue;
|
|
2181
|
+
if (aia.fetches >= aia.maxFetches) break;
|
|
2182
|
+
intrinsic.setAdd(aia.fetchedUrls, uris[k]);
|
|
3445
2183
|
aia.fetches += 1;
|
|
3446
2184
|
var certs;
|
|
3447
2185
|
try { certs = await _aiaFetchOne(uris[k], aia); }
|
|
3448
|
-
catch (_e2) { continue; }
|
|
3449
|
-
// Each response is already capped to maxCertsPerResponse by parseCertsOnly (per RESPONSE), so an earlier
|
|
3450
|
-
// URL's bundle never consumes a later URL's allowance -- coerce every returned cert.
|
|
2186
|
+
catch (_e2) { continue; }
|
|
3451
2187
|
for (var c = 0; c < certs.length; c++) {
|
|
3452
2188
|
var parsed;
|
|
3453
2189
|
try { parsed = coerceCert(certs[c]); }
|
|
@@ -3526,11 +2262,6 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
3526
2262
|
* });
|
|
3527
2263
|
* result.valid; // true when a path to a trust anchor was found and validated
|
|
3528
2264
|
*/
|
|
3529
|
-
// pki.path.build takes its own options AND forwards every pki.path.validate option to each
|
|
3530
|
-
// internal validate() call. The documented contract above says so, and validateOpts() copies
|
|
3531
|
-
// them wholesale. So the accepted set is the union, derived from _VALIDATE_OPTS. Retyping it
|
|
3532
|
-
// would let the two lists drift the first time validate gains an option. Both verbs now spell the
|
|
3533
|
-
// anchor option `trustAnchors`, so the union carries one key and build applies its own per-candidate.
|
|
3534
2265
|
var _BUILD_OPTS = (function () {
|
|
3535
2266
|
var o = {
|
|
3536
2267
|
aiaTimeout: 1, candidates: 1, fetchAia: 1, intermediates: 1, maxAiaFetches: 1,
|
|
@@ -3552,17 +2283,11 @@ async function build(leaf, opts) {
|
|
|
3552
2283
|
var poolInput = opts.candidates !== undefined ? opts.candidates : opts.intermediates;
|
|
3553
2284
|
if (poolInput === undefined) poolInput = [];
|
|
3554
2285
|
if (!intrinsic.isArray(poolInput)) throw E("path/bad-input", "build: opts.candidates must be an array of certificates");
|
|
3555
|
-
// A pool larger than the absolute ceiling is rejected at entry (bounds the
|
|
3556
|
-
// parse work on an untrusted bundle) -- distinct from the per-search tick
|
|
3557
|
-
// budget below, which an operator may set lower to cap the DFS.
|
|
3558
2286
|
var poolCeiling = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES;
|
|
3559
2287
|
if (poolInput.length > poolCeiling) throw E("path/bad-input", "build: the candidate pool has " + poolInput.length + " certificates, exceeding the " + poolCeiling + " ceiling");
|
|
3560
2288
|
var pool;
|
|
3561
2289
|
try { pool = poolInput.map(coerceCert); }
|
|
3562
2290
|
catch (e) { throw E("path/bad-input", "build: a candidate certificate did not parse", e); }
|
|
3563
|
-
// Byte-exact identities of every cert already in the shared pool, so an AIA-fetched duplicate (a mirror URL or a
|
|
3564
|
-
// repeating CMS returning the same issuer) is appended AT MOST ONCE -- it otherwise inflates the pool, charging
|
|
3565
|
-
// the candidate budget and the ceiling for each copy before the issuer is ever evaluated.
|
|
3566
2291
|
var poolDerKeys = new Set();
|
|
3567
2292
|
for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
|
|
3568
2293
|
|
|
@@ -3570,172 +2295,82 @@ async function build(leaf, opts) {
|
|
|
3570
2295
|
var anchors = opts.trustAnchors.map(function (a) { return toAnchor(a, "build"); });
|
|
3571
2296
|
|
|
3572
2297
|
guard.time.assertValid(opts.time, E, "path/bad-input", "build: opts.time (the validity-window check date, forwarded to validate)");
|
|
3573
|
-
// The chain-length bound is tied to the EFFECTIVE maxPathCerts the interleaved
|
|
3574
|
-
// validate uses: a built path holds hop + 1 certificates (the leaf plus each
|
|
3575
|
-
// intermediate), so capping hops at maxPathCerts - 1 keeps every path validate
|
|
3576
|
-
// sees within its own per-path ceiling -- otherwise a maxDepth of maxPathCerts
|
|
3577
|
-
// would assemble a maxPathCerts + 1 path that validate rejects with
|
|
3578
|
-
// path/bad-input. The same cap value is computed the way validate computes it
|
|
3579
|
-
// (opts.maxPathCerts forwarded), and the explicit-stack search below carries no
|
|
3580
|
-
// native-recursion stack risk regardless of the depth.
|
|
3581
2298
|
var effectiveMaxCerts = guard.limits.cap(opts.maxPathCerts, "build: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
3582
|
-
// depthCeiling is 0 when maxPathCerts is 1: the only path within the limit is a
|
|
3583
|
-
// single leaf directly under an anchor (a zero-hop search). maxDepth's minimum
|
|
3584
|
-
// is therefore 0, not 1 -- a zero-hop search is legitimate and a maxPathCerts of
|
|
3585
|
-
// 1 must not throw before the anchor is even checked.
|
|
3586
2299
|
var depthCeiling = effectiveMaxCerts - 1;
|
|
2300
|
+
// allow:guard-shape-reinlined -- opts.maxDepth here is the path-building chain depth (capped via guard.limits.cap against the PATH_BUILD_MAX_DEPTH ceiling), a different concept from the decode-recursion depth guard.limits.depthCap enforces
|
|
3587
2301
|
var maxDepth = guard.limits.cap(opts.maxDepth, "build: opts.maxDepth", Math.min(constants.LIMITS.PATH_BUILD_MAX_DEPTH, depthCeiling), { E: E, code: "path/bad-input", min: 0, max: depthCeiling });
|
|
3588
2302
|
var maxConsidered = guard.limits.cap(opts.maxCandidatesConsidered, "build: opts.maxCandidatesConsidered", poolCeiling, { E: E, code: "path/bad-input", min: 1 });
|
|
3589
2303
|
var doValidate = opts.validate !== false;
|
|
3590
2304
|
|
|
3591
|
-
// AIA caIssuers fetching is OFF unless opts.fetchAia === true -- the default build is byte-identical to
|
|
3592
|
-
// today's offline search (no transport constructed, no socket, no new path executed). When on, opts.transport
|
|
3593
|
-
// is the injectable seam (tests drive it offline); with no injected transport the default https transport
|
|
3594
|
-
// fails closed unless opts.tls carries an anchor / useSystemStore. opts.tls is the TLS trust for the AIA
|
|
3595
|
-
// HTTPS host -- DISTINCT from opts.trustAnchors (the PKI trust store the built path validates against).
|
|
3596
2305
|
var aiaCtx = null;
|
|
3597
2306
|
if (opts.fetchAia === true) {
|
|
3598
|
-
// Validate the AIA-specific options at CONFIG time (a caller typo is a path/bad-input throw, tier 1), not
|
|
3599
|
-
// lazily inside the fetch where a bad transport / timeout would be caught as an ordinary fetch fault and
|
|
3600
|
-
// silently degrade to path/no-path. A non-function transport can never be called; an injected transport gets
|
|
3601
|
-
// a validated timeout (it may not cap the value itself).
|
|
3602
2307
|
if (opts.transport !== undefined && typeof opts.transport !== "function") {
|
|
3603
2308
|
throw E("path/bad-input", "build: opts.transport must be a transport function (request) -> Promise<{ status, headers, body }>");
|
|
3604
2309
|
}
|
|
3605
|
-
// Validate aiaTimeout against the SAME bounds the built-in transport enforces (integer, 1..MAX_TIMEOUT) at
|
|
3606
|
-
// CONFIG time, so a non-integer / over-ceiling value is a path/bad-input throw here rather than a transport
|
|
3607
|
-
// rejection swallowed later as a fetch fault. guard.limits.cap is the exact primitive the transport applies.
|
|
3608
2310
|
if (opts.aiaTimeout !== undefined) {
|
|
3609
2311
|
guard.limits.cap(opts.aiaTimeout, "build: opts.aiaTimeout", 1, { E: E, code: "path/bad-input", min: 1, max: httpTransport.MAX_TIMEOUT });
|
|
3610
2312
|
}
|
|
3611
2313
|
var aiaTransport = opts.transport || httpTransport.https({ E: E, errPrefix: "path" });
|
|
3612
2314
|
aiaCtx = {
|
|
3613
2315
|
transport: aiaTransport,
|
|
3614
|
-
// SSRF for a DNS-name AIA host needs resolution-time address filtering + pinning, which only a transport
|
|
3615
|
-
// that ADVERTISES the capability performs (the built-in, or an injected one that sets blocksPrivateAddresses
|
|
3616
|
-
// to vouch it filters). An injected fn(request) that ignores the flag is treated as UNGUARDED: a DNS-name AIA
|
|
3617
|
-
// URL is then fail-closed (skipped) and only an IP literal (validated by the literal pre-check) is fetched.
|
|
3618
2316
|
transportGuardsAddresses: aiaTransport.blocksPrivateAddresses === true,
|
|
3619
2317
|
tls: opts.tls || {},
|
|
3620
|
-
timeout: opts.aiaTimeout,
|
|
3621
|
-
// Default the AIA response cap BELOW the general HTTP ceiling (a caIssuers response is small); tighten
|
|
3622
|
-
// downward only, never above HTTP_MAX_RESPONSE_BYTES.
|
|
2318
|
+
timeout: opts.aiaTimeout,
|
|
3623
2319
|
maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "build: opts.maxResponseBytes", constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES, { E: E, code: "path/bad-input", min: 1, max: constants.LIMITS.PATH_AIA_MAX_RESPONSE_BYTES }),
|
|
3624
2320
|
maxPerCert: guard.limits.cap(opts.maxAiaPerCert, "build: opts.maxAiaPerCert", constants.LIMITS.PATH_AIA_MAX_PER_CERT, { E: E, code: "path/bad-input", min: 0 }),
|
|
3625
2321
|
maxCertsPerResponse: constants.LIMITS.PATH_AIA_MAX_CERTS_PER_RESPONSE,
|
|
3626
|
-
// The total fetch budget is a SILENT cap (stop initiating fetches when reached), NOT a throw: a bound on
|
|
3627
|
-
// an ADVISORY fetch must never abort a build that the static pool could still complete. maxFetches may be
|
|
3628
|
-
// 0 (fetchAia:true but no network -- identical to offline).
|
|
3629
2322
|
maxFetches: guard.limits.cap(opts.maxAiaFetches, "build: opts.maxAiaFetches", constants.LIMITS.PATH_AIA_MAX_FETCHES, { E: E, code: "path/bad-input", min: 0 }),
|
|
3630
2323
|
fetchedUrls: new Set(),
|
|
3631
2324
|
fetches: 0,
|
|
3632
2325
|
};
|
|
3633
2326
|
}
|
|
3634
2327
|
|
|
3635
|
-
// The build-specific options are consumed here; every remaining validate
|
|
3636
|
-
// option is forwarded unchanged to the interleaved validate call. Object.keys
|
|
3637
|
-
// enumerates only own enumerable properties, so no prototype-pollution belt.
|
|
3638
2328
|
var BUILD_ONLY_OPT = { candidates: 1, intermediates: 1, trustAnchors: 1, maxDepth: 1, maxCandidatesConsidered: 1, validate: 1,
|
|
3639
2329
|
fetchAia: 1, transport: 1, tls: 1, maxAiaFetches: 1, maxAiaPerCert: 1, aiaTimeout: 1, maxResponseBytes: 1 };
|
|
3640
|
-
// No prototype, so the set forwarded to each internal validate holds what was put in it and
|
|
3641
|
-
// nothing a runtime carries. A plain `{}` here re-acquired a polluted name after the caller's
|
|
3642
|
-
// own value had been dropped, which is how an explicit `softFail: false` became a waiver.
|
|
3643
2330
|
var forwarded = Object.create(null);
|
|
3644
|
-
// Driven by the contract. Walk the validate options this verb forwards and take the ones the
|
|
3645
|
-
// bag answers for.
|
|
3646
|
-
//
|
|
3647
|
-
// Enumerating the bag reads whatever a property walk chooses to report, and that walk skips a
|
|
3648
|
-
// method so a caller's class keeps its own. A known option whose value were a bare function
|
|
3649
|
-
// would be indistinguishable from one, and a defaults-style bag like
|
|
3650
|
-
// `Object.create({ someOption: fn })` would have it dropped here, leaving each internal
|
|
3651
|
-
// validate running without something the caller supplied.
|
|
3652
|
-
//
|
|
3653
|
-
// No option takes a bare function today. `revocationChecker` is `{ check }`, `verifier` is
|
|
3654
|
-
// `{ verify }`, and `transport` is build-only and never forwarded. So this is a hardening
|
|
3655
|
-
// rather than a live defect, and there is no vector that fails without it. It is here because
|
|
3656
|
-
// asking the contract cannot drop a known option whatever its value, and the alternative makes
|
|
3657
|
-
// the forwarded set depend on a walk whose rules exist for an unrelated question.
|
|
3658
|
-
//
|
|
3659
|
-
// `in` rather than an own check, because an option carried on a prototype is one the caller
|
|
3660
|
-
// supplied and `opts.softFail` answers for it. Own membership decides only whether a name is
|
|
3661
|
-
// build-only, which is a question about this module's own table and never about the caller.
|
|
3662
2331
|
Object.keys(_VALIDATE_OPTS).forEach(function (k) {
|
|
3663
2332
|
if (intrinsic.hasOwn(BUILD_ONLY_OPT, k)) return;
|
|
3664
2333
|
if (k in opts) forwarded[k] = opts[k];
|
|
3665
2334
|
});
|
|
3666
2335
|
function validateOpts(anchor) {
|
|
3667
|
-
var vo = Object.create(null);
|
|
3668
|
-
// Every name in the forwarded set, which was built from the contract above and has no
|
|
3669
|
-
// prototype, so its own keys are exactly what was put in it. A property walk here would
|
|
3670
|
-
// apply the method rule a second time and drop a function-valued option all over again;
|
|
3671
|
-
// forwarding less than was validated is worse than refusing the bag.
|
|
2336
|
+
var vo = Object.create(null);
|
|
3672
2337
|
Object.keys(forwarded).forEach(function (f) { vo[f] = forwarded[f]; });
|
|
3673
|
-
vo.trustAnchors = anchor;
|
|
2338
|
+
vo.trustAnchors = anchor;
|
|
3674
2339
|
return vo;
|
|
3675
2340
|
}
|
|
3676
2341
|
|
|
3677
|
-
// The DoS terminator: tick() once per candidate-issuer expansion so a hostile
|
|
3678
|
-
// mesh cannot fan the search without bound (RFC 4158 8.1). A breach throws
|
|
3679
|
-
// path/build-limit. candidatesConsidered mirrors the tick count for the caller.
|
|
3680
2342
|
var counter = guard.limits.counter(maxConsidered, E, "path/build-limit", "build: candidate-issuer expansion");
|
|
3681
2343
|
var considered = 0;
|
|
3682
2344
|
var anyChainAssembled = false;
|
|
3683
2345
|
var bestFail = null;
|
|
3684
2346
|
var success = null;
|
|
3685
2347
|
|
|
3686
|
-
// A bounded, EXPLICIT-stack forward DFS from the leaf toward an anchor -- no
|
|
3687
|
-
// native recursion, so an operator-raised maxDepth cannot overflow the stack
|
|
3688
|
-
// over an untrusted graph. Each frame is a candidate chain [top, ...toward the
|
|
3689
|
-
// leaf] (leaf last) plus the identity keys already on it; a frame is expanded
|
|
3690
|
-
// by prepending a name-chaining issuer. Highest-priority candidates are pushed
|
|
3691
|
-
// LAST so they are popped (explored) FIRST -- depth-first in priority order.
|
|
3692
|
-
// Caps are enforced before every expansion; a candidate whose (subject, SAN,
|
|
3693
|
-
// public key) tuple is already on the chain is a loop and is pruned.
|
|
3694
2348
|
var stack = [{ chain: [leafCert], hop: 0, keys: new Set([identityKey(leafCert)]) }];
|
|
3695
|
-
// AIA fetch frames are DEFERRED into this queue and drained ONLY when `stack` is empty -- i.e. once the ENTIRE
|
|
3696
|
-
// local (static-pool) search has failed. This enforces RFC 4158 sec. 7.2 "local before remote" GLOBALLY across
|
|
3697
|
-
// the whole search, not per-branch: a build the static pool can complete never issues a network request, and a
|
|
3698
|
-
// higher-priority local dead-end can never fetch ahead of a still-unexplored lower-priority static sibling.
|
|
3699
|
-
// Drained DEEPEST-first, and among equal depth EARLIEST-deferred (= highest DFS priority) first: the actual
|
|
3700
|
-
// dead end (a missing deeper hop) is fetched before an ancestor whose issuer the pool ALREADY supplies (so a
|
|
3701
|
-
// scarce budget is never spent re-retrieving a locally-resolved hop), and a higher-scoring sibling's AIA is
|
|
3702
|
-
// tried before a lower-scoring one (a plain LIFO pop would reverse sibling priority and let a stale low-priority
|
|
3703
|
-
// branch's dead URL waste a tight budget before the preferred branch).
|
|
3704
2349
|
var deferredAia = [];
|
|
3705
|
-
var deferSeq = 0;
|
|
2350
|
+
var deferSeq = 0;
|
|
3706
2351
|
while (!success) {
|
|
3707
2352
|
if (!stack.length) {
|
|
3708
|
-
// The local search is drained. Begin / continue the fetch phase: pick a deferred frame with PENDING WORK --
|
|
3709
|
-
// one that has NOT yet fetched, OR whose poolMark trails the shared pool (a SIBLING branch fetched a cert
|
|
3710
|
-
// SINCE this frame last ran, which may complete it). A drained frame is NOT discarded: a later sibling's
|
|
3711
|
-
// fetch can add the very issuer an earlier, already-run branch needed, so every frame stays eligible for
|
|
3712
|
-
// pool growth until it has fetched AND seen the whole pool. Among eligible frames pick the deepest, then the
|
|
3713
|
-
// earliest-deferred (highest DFS priority). No eligible frame -> the whole search is exhausted.
|
|
3714
2353
|
var _bi = -1;
|
|
3715
2354
|
for (var _di = 0; _di < deferredAia.length; _di++) {
|
|
3716
2355
|
var _f = deferredAia[_di];
|
|
3717
|
-
if (_f.fetched && _f.poolMark >= pool.length) continue;
|
|
2356
|
+
if (_f.fetched && _f.poolMark >= pool.length) continue;
|
|
3718
2357
|
if (_bi === -1) { _bi = _di; continue; }
|
|
3719
2358
|
var _bf = deferredAia[_bi];
|
|
3720
2359
|
if (_f.hop > _bf.hop || (_f.hop === _bf.hop && _f.seq < _bf.seq)) _bi = _di;
|
|
3721
2360
|
}
|
|
3722
|
-
if (_bi === -1) break;
|
|
3723
|
-
var fb = deferredAia[_bi];
|
|
2361
|
+
if (_bi === -1) break;
|
|
2362
|
+
var fb = deferredAia[_bi];
|
|
3724
2363
|
var fbCur = fb.chain[0];
|
|
3725
2364
|
if (!fb.fetched && aiaCtx && aiaCtx.fetches < aiaCtx.maxFetches) {
|
|
3726
|
-
var fetched = await _fetchAiaIssuers(fbCur, aiaCtx);
|
|
2365
|
+
var fetched = await _fetchAiaIssuers(fbCur, aiaCtx);
|
|
3727
2366
|
for (var fj = 0; fj < fetched.length; fj++) {
|
|
3728
2367
|
var fdk = certDerKey(fetched[fj]);
|
|
3729
|
-
if (intrinsic.setHas(poolDerKeys, fdk)) continue;
|
|
2368
|
+
if (intrinsic.setHas(poolDerKeys, fdk)) continue;
|
|
3730
2369
|
intrinsic.setAdd(poolDerKeys, fdk);
|
|
3731
2370
|
if (pool.length < poolCeiling) pool.push(fetched[fj]);
|
|
3732
2371
|
}
|
|
3733
2372
|
}
|
|
3734
|
-
fb.fetched = true;
|
|
3735
|
-
// Re-expand fb against every pool cert added SINCE it last ran -- its OWN just-fetched certs AND any a SIBLING
|
|
3736
|
-
// branch fetched into the shared pool. The pool-index mark skips certs fb already scored (no redundant work);
|
|
3737
|
-
// advancing it means fb re-scores only certs added even later. This runs even when the budget is exhausted, so
|
|
3738
|
-
// a budget-capped frame still benefits from a sibling fetch.
|
|
2373
|
+
fb.fetched = true;
|
|
3739
2374
|
var fbAki = childAkiKeyId(fbCur);
|
|
3740
2375
|
var fbScored = [];
|
|
3741
2376
|
for (var pj = fb.poolMark; pj < pool.length; pj++) {
|
|
@@ -3750,9 +2385,6 @@ async function build(leaf, opts) {
|
|
|
3750
2385
|
var frame = stack.pop();
|
|
3751
2386
|
var current = frame.chain[0];
|
|
3752
2387
|
|
|
3753
|
-
// Terminate: is `current` issued by a configured anchor? An anchor-adjacent
|
|
3754
|
-
// cert completes a candidate path; validate is the authority on acceptance.
|
|
3755
|
-
// A found path breaks out here and the enclosing while exits on !success.
|
|
3756
2388
|
for (var ai = 0; ai < anchors.length; ai++) {
|
|
3757
2389
|
if (!nameMatchSoft(current.issuer.rdns, anchors[ai].name.rdns)) continue;
|
|
3758
2390
|
if (!doValidate) { success = { path: frame.chain.slice(), trustAnchor: anchors[ai] }; break; }
|
|
@@ -3761,7 +2393,7 @@ async function build(leaf, opts) {
|
|
|
3761
2393
|
if (res.valid) { success = { valid: true, path: frame.chain.slice(), trustAnchor: anchors[ai], result: res }; break; }
|
|
3762
2394
|
if (!bestFail) bestFail = { path: frame.chain.slice(), trustAnchor: anchors[ai], result: res };
|
|
3763
2395
|
}
|
|
3764
|
-
if (success || frame.hop >= maxDepth) continue;
|
|
2396
|
+
if (success || frame.hop >= maxDepth) continue;
|
|
3765
2397
|
|
|
3766
2398
|
var childAki = childAkiKeyId(current);
|
|
3767
2399
|
var scored = [];
|
|
@@ -3770,35 +2402,21 @@ async function build(leaf, opts) {
|
|
|
3770
2402
|
scored.push({ cand: pool[pi], score: scoreCandidate(pool[pi], childAki, anchors, opts.time) });
|
|
3771
2403
|
}
|
|
3772
2404
|
}
|
|
3773
|
-
// Opt-in AIA caIssuers fetch as a FALLBACK: DEFER it (drained above only after the ENTIRE local search is
|
|
3774
|
-
// exhausted, RFC 4158 sec. 7.2 "local before remote") rather than pushing it onto the stack, so it can never
|
|
3775
|
-
// run ahead of an unexplored local sibling -- a build the static pool can complete never fetches. It is gated
|
|
3776
|
-
// on `success` being unset (guaranteed by the continue above), NOT on the issuer name failing to match an
|
|
3777
|
-
// anchor: an issuer DN that matches an anchor whose KEY did not validate this chain (a CA key rollover -- the
|
|
3778
|
-
// real same-DN, different-key intermediate is missing and reachable only via AIA) still needs the fetch.
|
|
3779
2405
|
if (aiaCtx && frame.hop < maxDepth && aiaCtx.fetches < aiaCtx.maxFetches) {
|
|
3780
|
-
// poolMark: the shared-pool length now, so the drain re-expands this frame only against certs added LATER
|
|
3781
|
-
// (its own fetch + any sibling branch's fetch), never re-scoring the static pool it expands against below.
|
|
3782
|
-
// seq: the deferral order, so the drain can tie-break same-depth frames by DFS priority (earliest first).
|
|
3783
|
-
// fetched: false until this frame's own AIA fetch runs; it then stays eligible for later sibling pool growth.
|
|
3784
2406
|
deferredAia.push({ chain: frame.chain, hop: frame.hop, keys: frame.keys, poolMark: pool.length, seq: deferSeq, fetched: false });
|
|
3785
2407
|
deferSeq += 1;
|
|
3786
2408
|
}
|
|
3787
2409
|
considered += _pushCandidates(frame, scored, stack, counter);
|
|
3788
2410
|
}
|
|
3789
2411
|
|
|
3790
|
-
var aiaFetches = aiaCtx ? aiaCtx.fetches : 0;
|
|
2412
|
+
var aiaFetches = aiaCtx ? aiaCtx.fetches : 0;
|
|
3791
2413
|
if (success) {
|
|
3792
2414
|
if (doValidate) return { valid: true, path: success.path, trustAnchor: success.trustAnchor, result: success.result, candidatesConsidered: considered, aiaFetches: aiaFetches };
|
|
3793
2415
|
return { path: success.path, trustAnchor: success.trustAnchor, candidatesConsidered: considered, aiaFetches: aiaFetches };
|
|
3794
2416
|
}
|
|
3795
2417
|
if (anyChainAssembled) {
|
|
3796
|
-
// Chains reached an anchor but none validated -> the soft verdict carrying
|
|
3797
|
-
// the best failing validate result (parity with validate; never a throw).
|
|
3798
2418
|
return { valid: false, path: bestFail.path, trustAnchor: bestFail.trustAnchor, result: bestFail.result, candidatesConsidered: considered, aiaFetches: aiaFetches };
|
|
3799
2419
|
}
|
|
3800
|
-
// No chain to any configured anchor could even be assembled -- a permanent
|
|
3801
|
-
// structural verdict (name/key chaining dead-ended before the trust store).
|
|
3802
2420
|
throw E("path/no-path", "build: no certification path from the leaf to any configured trust anchor could be assembled");
|
|
3803
2421
|
}
|
|
3804
2422
|
|
|
@@ -3809,13 +2427,6 @@ module.exports = {
|
|
|
3809
2427
|
crlChecker: crlChecker,
|
|
3810
2428
|
ocspChecker: ocspChecker,
|
|
3811
2429
|
verifyOcspResponse: verifyOcspResponse,
|
|
3812
|
-
// The set of extension OIDs whose CRITICAL semantics this validator processes (RFC 5280 sec. 6.1).
|
|
3813
|
-
// Exposed so a linter can distinguish "processed" from "merely decoded" and stay consistent with
|
|
3814
|
-
// the path-validation verdict on a critical extension -- a decoder in certExtensionDecoders is NOT
|
|
3815
|
-
// by itself proof the criticality is honored. Both sets are frozen so a caller cannot mutate them.
|
|
3816
2430
|
PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
|
|
3817
|
-
// Extensions that ARE processed for an intermediate CA but are unprocessed on the target/leaf, so a
|
|
3818
|
-
// critical instance on the target fails closed (RFC 5280 sec. 6.1.5(f)) -- policyMappings is
|
|
3819
|
-
// prepare-next-only and SHOULD-be-non-critical. A linter mirrors this for a leaf certificate.
|
|
3820
2431
|
TARGET_UNPROCESSED_IF_CRITICAL: TARGET_UNPROCESSED_IF_CRITICAL,
|
|
3821
2432
|
};
|