@blamejs/pki 0.8.6 → 0.8.9
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 +143 -76
- package/README.md +4 -2
- package/index.js +11 -0
- package/lib/acme.js +15 -78
- package/lib/asn1-der.js +58 -6
- package/lib/cbor-det.js +11 -1
- package/lib/cmc-build.js +1 -1
- package/lib/cmc-verify.js +3 -3
- package/lib/cmp-session.js +1 -1
- package/lib/cms-sign.js +3 -3
- package/lib/cms-verify.js +5 -5
- package/lib/constants.js +11 -0
- package/lib/est.js +1 -1
- package/lib/framework-error.js +8 -0
- package/lib/guard-async.js +53 -2
- package/lib/guard-encoding.js +105 -1
- package/lib/guard-identifier.js +74 -1
- package/lib/guard-intrinsic.js +2 -0
- package/lib/guard-json.js +1 -6
- package/lib/guard-limits.js +40 -1
- package/lib/guard-list.js +25 -0
- package/lib/guard-name.js +134 -22
- package/lib/guard-text.js +33 -1
- package/lib/http-transport.js +39 -7
- package/lib/identity-match.js +561 -0
- package/lib/ip-utils.js +52 -1
- package/lib/key.js +250 -5
- package/lib/ocsp.js +125 -22
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +199 -61
- package/lib/pkcs11-uri.js +646 -0
- package/lib/pkcs12-build.js +6 -1
- package/lib/revocation-fetch.js +772 -0
- package/lib/schema-all.js +124 -0
- package/lib/schema-attrcert.js +4 -2
- package/lib/schema-crmf.js +4 -2
- package/lib/schema-pkcs1.js +335 -0
- package/lib/schema-pkcs8.js +15 -0
- package/lib/schema-pkix.js +322 -40
- package/lib/schema-sec1.js +244 -0
- package/lib/schema-tsp.js +4 -2
- package/lib/schema-x509.js +4 -2
- package/lib/trust.js +2 -2
- package/lib/tsp-sign.js +1 -1
- package/lib/webauthn.js +4 -4
- package/lib/webcrypto.js +17 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var net = require("net");
|
|
6
|
+
var asn1 = require("./asn1-der");
|
|
7
|
+
var constants = require("./constants");
|
|
8
|
+
var oid = require("./oid");
|
|
9
|
+
var guard = require("./guard-all");
|
|
10
|
+
var intrinsic = require("./guard-intrinsic");
|
|
11
|
+
var frameworkError = require("./framework-error");
|
|
12
|
+
var pkix = require("./schema-pkix");
|
|
13
|
+
var ocsp = require("./ocsp");
|
|
14
|
+
var crlSchema = require("./schema-crl");
|
|
15
|
+
var crlVerify = require("./crl-verify");
|
|
16
|
+
var httpTransport = require("./http-transport");
|
|
17
|
+
var mime = require("./mime");
|
|
18
|
+
var pathValidate = require("./path-validate");
|
|
19
|
+
var ocspSchema = require("./schema-ocsp");
|
|
20
|
+
|
|
21
|
+
var PathError = frameworkError.PathError;
|
|
22
|
+
function E(code, message, cause) { return new PathError(code, message, cause); }
|
|
23
|
+
|
|
24
|
+
var NS = pkix.makeNS("path", PathError, oid);
|
|
25
|
+
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
26
|
+
|
|
27
|
+
var OID_CDP = oid.byName("cRLDistributionPoints");
|
|
28
|
+
var OID_FRESHEST = oid.byName("freshestCRL");
|
|
29
|
+
var OID_AIA = oid.byName("authorityInfoAccess");
|
|
30
|
+
var OID_AD_OCSP = oid.byName("ocsp");
|
|
31
|
+
var OID_OCSP_NONCE = oid.byName("ocspNonce");
|
|
32
|
+
|
|
33
|
+
var _append = guard.list.append;
|
|
34
|
+
var LIMITS = constants.LIMITS;
|
|
35
|
+
|
|
36
|
+
var CHECKER_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
37
|
+
transport: 1, allowPlaintextHttp: 1, useDeltas: 1, nonce: 1, tls: 1,
|
|
38
|
+
maxFetches: 1, maxPerCert: 1, maxResponseBytes: 1, totalDeadlineMs: 1,
|
|
39
|
+
sourceTimeoutMs: 1, retries: 1, toleranceMs: 1, cache: 1,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** @internal A candidate is `{ kind, url, reasons }`, where `kind` says which parser the body goes
|
|
43
|
+
* to. The structure is decided by where the URL came from and never by what came back, so a
|
|
44
|
+
* Content-Type header cannot choose a parser. */
|
|
45
|
+
var KIND_CRL = "crl";
|
|
46
|
+
var KIND_OCSP = "ocsp";
|
|
47
|
+
|
|
48
|
+
var TRANSPORT_SHAPE = "(request) => Promise<{ status, headers, body }>";
|
|
49
|
+
|
|
50
|
+
/** @internal An extension the certificate may not carry, and may carry malformed. A decode fault is
|
|
51
|
+
* a reason rather than a throw, because this runs inside a checker whose every outcome is a
|
|
52
|
+
* verdict (RFC 5280 sec. 6.3.3 closing paragraph: an undetermined status, never an unrevoked one). */
|
|
53
|
+
/** @internal An extension that does not decode names no URL, which is not the same as naming none:
|
|
54
|
+
* a certificate carrying an unreadable cRLDistributionPoints has a revocation location an operator
|
|
55
|
+
* cannot reach, and one carrying no such extension has none to reach. Both end at an undetermined
|
|
56
|
+
* status, so the difference lives in the reason, and the refusal is recorded as a skip rather than
|
|
57
|
+
* swallowed. */
|
|
58
|
+
function _softDecode(holder, extOid, skips) {
|
|
59
|
+
var list = holder.extensions || holder.crlExtensions || [];
|
|
60
|
+
var found = guard.list.anyMatches(list, function (e) { return !!e && e.oid === extOid; })
|
|
61
|
+
? _firstWithOid(list, extOid) : null;
|
|
62
|
+
if (found === null) return null;
|
|
63
|
+
try { return EXT_DECODERS[extOid](found.value); }
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (skips) _append(skips, "the " + (oid.name(extOid) || extOid) + " extension does not decode: " + _say(e));
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _firstWithOid(list, extOid) {
|
|
71
|
+
for (var i = 0; i < list.length; i++) {
|
|
72
|
+
if (list[i] && list[i].oid === extOid) return list[i];
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @internal RFC 5280 sec. 4.2.1.13 classification, on the DER shape and never on a string. A
|
|
78
|
+
* distribution point is fetchable over HTTP only as a `fullName` carrying a `uniformResourceIdentifier`
|
|
79
|
+
* whose scheme this client speaks. Everything else is a skip with its reason named, which is what
|
|
80
|
+
* keeps "no fetchable distribution point" distinct from "the fetch failed". */
|
|
81
|
+
function _crlCandidates(points, net_, skips) {
|
|
82
|
+
var out = [];
|
|
83
|
+
if (!intrinsic.isArray(points)) return out;
|
|
84
|
+
for (var i = 0; i < points.length; i++) {
|
|
85
|
+
var dp = points[i];
|
|
86
|
+
if (!dp.distributionPoint) {
|
|
87
|
+
_append(skips, dp.cRLIssuer ? "a distribution point names a cRLIssuer and no location" :
|
|
88
|
+
"a distribution point names no location");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (dp.distributionPoint.kind !== "fullName") {
|
|
92
|
+
_append(skips, "a distribution point uses nameRelativeToCRLIssuer, which names no location");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
var names = dp.distributionPoint.names || [];
|
|
96
|
+
var tookOne = false;
|
|
97
|
+
for (var n = 0; n < names.length; n++) {
|
|
98
|
+
var gn = _generalName(names[n]);
|
|
99
|
+
if (gn === null || gn.tag !== 6) { _append(skips, "a distribution point name is not a URI"); continue; }
|
|
100
|
+
var got = _acceptableUrl(gn.value, net_);
|
|
101
|
+
if (got.url === null) { _append(skips, "a distribution point URI " + got.why + ": " + guard.text.showValue(gn.value)); continue; }
|
|
102
|
+
_append(out, { kind: KIND_CRL, url: got.url, reasons: dp.reasons || null });
|
|
103
|
+
tookOne = true;
|
|
104
|
+
}
|
|
105
|
+
if (!tookOne && names.length === 0) _append(skips, "a distribution point carries an empty fullName");
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @internal The decoded fullName entry is the GeneralName bytes; the decoder surfaces the value
|
|
111
|
+
* when it was asked to decode one, and the raw bytes when it was not. Both shapes are read here so
|
|
112
|
+
* the caller of this module does not have to know which drive the extension took. */
|
|
113
|
+
function _generalName(entry) {
|
|
114
|
+
if (!entry) return null;
|
|
115
|
+
if (typeof entry === "object" && typeof entry.tag === "number") return { tag: entry.tag, value: entry.value };
|
|
116
|
+
if (typeof entry === "object" && typeof entry.tagNumber === "number") return { tag: entry.tagNumber, value: entry.value };
|
|
117
|
+
if (guard.bytes.isByteSource(entry)) {
|
|
118
|
+
/** @internal The fullName entries arrive as the GeneralName's own DER, so the tag and the
|
|
119
|
+
* content come from the codec. Reading the length by hand would be right only while every URI
|
|
120
|
+
* stayed under 128 bytes. */
|
|
121
|
+
var node;
|
|
122
|
+
try { node = asn1.decode(guard.bytes.source(entry, PathError, "path/bad-input", "a distribution point name")); }
|
|
123
|
+
catch (_e) { return null; } // allow:swallow-unverified a name that does not decode names no location
|
|
124
|
+
if (node.tagClass !== "context") return null;
|
|
125
|
+
return { tag: node.tagNumber, value: intrinsic.bufToString(node.content, "latin1") };
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @internal RFC 5280 sec. 4.2.2.1 puts the responder at an AccessDescription whose accessMethod is
|
|
131
|
+
* id-ad-ocsp and whose accessLocation is a URI. No CRL URL is ever taken from this extension, which
|
|
132
|
+
* that section states outright. */
|
|
133
|
+
function _ocspCandidates(descriptions, net_, skips) {
|
|
134
|
+
var out = [];
|
|
135
|
+
if (!intrinsic.isArray(descriptions)) return out;
|
|
136
|
+
for (var i = 0; i < descriptions.length; i++) {
|
|
137
|
+
var ad = descriptions[i];
|
|
138
|
+
if (ad.accessMethod !== OID_AD_OCSP) continue;
|
|
139
|
+
if (!ad.accessLocation || ad.accessLocation.tag !== 6) {
|
|
140
|
+
_append(skips, "an id-ad-ocsp access location is not a URI (RFC 5280 sec. 4.2.2.1)");
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
var got = _acceptableUrl(ad.accessLocation.value, net_);
|
|
144
|
+
if (got.url === null) { _append(skips, "an id-ad-ocsp URI " + got.why + ": " + guard.text.showValue(ad.accessLocation.value)); continue; }
|
|
145
|
+
_append(out, { kind: KIND_OCSP, url: got.url, reasons: null });
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @internal The URL this client will open, with the clause naming why it will not when it will
|
|
151
|
+
* not. The scheme decides first, then the SSRF rules the AIA route already applies: no private,
|
|
152
|
+
* loopback or link-local destination, and a hostname only where the transport filters the address
|
|
153
|
+
* it resolved. The fragment is dropped, since it is never sent.
|
|
154
|
+
*
|
|
155
|
+
* An address literal is decided here, because the destination is in the URL. A name is not: what
|
|
156
|
+
* it resolves to is decided inside whoever opens the socket, so a certificate naming
|
|
157
|
+
* `crl.internal.example` reaches a private network unless that resolution is filtered and pinned.
|
|
158
|
+
* The transport `pki.transport.https(defaults)` returns declares `blocksPrivateAddresses` and does
|
|
159
|
+
* both; a transport that declares nothing is never handed a name. The declaration is read off the
|
|
160
|
+
* transport's own slot, because `Function.prototype` carries whatever has been written to it for
|
|
161
|
+
* every function in the process. */
|
|
162
|
+
function _acceptableUrl(value, net_) {
|
|
163
|
+
if (typeof value !== "string" || value === "") return _no("is not one this client fetches");
|
|
164
|
+
var u;
|
|
165
|
+
try { u = new URL(value); }
|
|
166
|
+
catch (_e) { return _no("is not one this client fetches"); } // allow:swallow-unverified a value that is not a URL names no location
|
|
167
|
+
if (u.protocol !== "https:" && !(net_.allowHttp && u.protocol === "http:")) return _no("is not one this client fetches");
|
|
168
|
+
var host = u.hostname;
|
|
169
|
+
if (host.charAt(0) === "[" && host.charAt(host.length - 1) === "]") host = host.slice(1, -1);
|
|
170
|
+
if (net.isIP(host) !== 0) {
|
|
171
|
+
if (httpTransport.isBlockedIp(host)) return _no("is not one this client fetches");
|
|
172
|
+
} else if (!net_.guardsAddresses) {
|
|
173
|
+
return _no("names a host whose resolved address opts.transport does not declare it filters");
|
|
174
|
+
}
|
|
175
|
+
u.hash = "";
|
|
176
|
+
return { url: u.href, why: null };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _no(why) { return { url: null, why: why }; }
|
|
180
|
+
|
|
181
|
+
/** @internal A switch that turns a security control on is read as the boolean it documents, never
|
|
182
|
+
* coerced. `nonce: "true"` written by an operator asking for replay binding would read as off
|
|
183
|
+
* under a `=== true` test, and the requests would go out carrying no nonce with nothing said. An
|
|
184
|
+
* absent value is the documented default; anything else is a configuration fault. */
|
|
185
|
+
function _booleanOption(value, label) {
|
|
186
|
+
if (value === undefined || value === null) return false;
|
|
187
|
+
if (value === true || value === false) return value;
|
|
188
|
+
throw E("path/bad-input", label + " must be true or false, got " + guard.text.showValue(value));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @internal Whether this transport says it filters the address it resolved. A transport is a
|
|
192
|
+
* function, so the flag is asked of the slot rather than of the chain: reading it through the
|
|
193
|
+
* transport finds a value on `Function.prototype`, which every function in the process carries and
|
|
194
|
+
* which nobody who wrote a transport declared. What the flag buys is a hostname destination taken
|
|
195
|
+
* from a certificate, so an undeclared transport being read as declaring it is the whole guard.
|
|
196
|
+
* @guard-via guard.identifier.declaresOwn */
|
|
197
|
+
function _declaresAddressGuard(transport) {
|
|
198
|
+
return guard.identifier.declaresOwn(transport, "blocksPrivateAddresses");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @internal Reuse is decided from the signed window and from nothing else: RFC 5019 sec. 7.5 has a
|
|
202
|
+
* client rely on the values in the signed response, and sec. 6.1 admits only a response whose
|
|
203
|
+
* signature validated. The HTTP caching headers are not read here at all, so they can neither
|
|
204
|
+
* extend an entry's life nor end it.
|
|
205
|
+
*
|
|
206
|
+
* Two key shapes, kept apart on purpose. An OCSP entry is keyed on the CertID tuple and the
|
|
207
|
+
* responder URL it was asked of; a CRL entry on the request URL and the encoded issuer name of the
|
|
208
|
+
* CRL that came back. Neither is the URL the socket was opened on, and neither is the encoded name
|
|
209
|
+
* the sec. 6.3.3(b) distribution point match reads, so a cache hit can never stand in for a name
|
|
210
|
+
* match. */
|
|
211
|
+
function _makeCache(maxEntries, maxBytes) {
|
|
212
|
+
var entries = new Map();
|
|
213
|
+
var bytes = 0;
|
|
214
|
+
function evictOldest() {
|
|
215
|
+
var it = entries.keys().next();
|
|
216
|
+
if (it.done) return;
|
|
217
|
+
var old = entries.get(it.value);
|
|
218
|
+
bytes -= old ? old.size : 0;
|
|
219
|
+
entries.delete(it.value);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
get: function (key, at) {
|
|
223
|
+
var e = entries.get(key);
|
|
224
|
+
if (!e) return null;
|
|
225
|
+
if (!(at < e.notAfter) || !(at >= e.notBefore)) { bytes -= e.size; entries.delete(key); return null; }
|
|
226
|
+
return e.value;
|
|
227
|
+
},
|
|
228
|
+
put: function (key, value, notBefore, notAfter, size) {
|
|
229
|
+
if (!(notAfter instanceof Date) || !(notBefore instanceof Date)) return;
|
|
230
|
+
if (size > maxBytes) return;
|
|
231
|
+
if (entries.has(key)) { bytes -= entries.get(key).size; entries.delete(key); }
|
|
232
|
+
while (entries.size >= maxEntries || bytes + size > maxBytes) {
|
|
233
|
+
if (entries.size === 0) return;
|
|
234
|
+
evictOldest();
|
|
235
|
+
}
|
|
236
|
+
entries.set(key, { value: value, notBefore: notBefore, notAfter: notAfter, size: size });
|
|
237
|
+
bytes += size;
|
|
238
|
+
},
|
|
239
|
+
size: function () { return entries.size; },
|
|
240
|
+
bytes: function () { return bytes; },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _ocspCacheKey(certId, url) {
|
|
245
|
+
return "ocsp|" + url + "|" + certId.hashAlgorithm.oid + "|" +
|
|
246
|
+
intrinsic.bufToString(certId.issuerNameHash, "hex") + "|" +
|
|
247
|
+
intrinsic.bufToString(certId.issuerKeyHash, "hex") + "|" + certId.serialNumberHex;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** @internal What identifies a kept list: the URL it came from, the issuer name it is about, and
|
|
251
|
+
* the key that has to verify it. A CA that rotates its key keeps both its name and its CRL URL,
|
|
252
|
+
* so neither tells the old list from the new one; an entry found on those two alone would be
|
|
253
|
+
* handed to a certificate under the new key, fail its signature check, and leave the status
|
|
254
|
+
* undetermined with no request made until the entry aged out. */
|
|
255
|
+
function _crlCacheKey(url, issuerBytes, keyBytes) {
|
|
256
|
+
return "crl|" + url + "|" + intrinsic.bufToString(issuerBytes, "hex") +
|
|
257
|
+
"|" + intrinsic.bufToString(guard.bytes.source(keyBytes, PathError, "path/bad-input", "the issuer public key"), "hex");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @internal One request. A non-200 is a refusal and no redirect is followed, which matches the AIA
|
|
261
|
+
* route: a 3xx would move the fetch to a location the certificate never named. The body is held to
|
|
262
|
+
* the response cap before anything parses it. */
|
|
263
|
+
async function _fetchOne(state, req, what) {
|
|
264
|
+
if (state.deadline.expired()) throw E("path/revocation-fetch-failed", "the revocation deadline passed before " + what);
|
|
265
|
+
state.fetches.tick();
|
|
266
|
+
var timeout = state.sourceTimeoutMs;
|
|
267
|
+
var left = state.deadline.remaining();
|
|
268
|
+
if (left < timeout) timeout = left;
|
|
269
|
+
/** @internal The `timeout` field asks the transport to stop; the bound here makes it stop. A
|
|
270
|
+
* transport is caller code and need not implement the field, and one that never settles would
|
|
271
|
+
* otherwise hold the validation open with no deadline reached, since the deadline is only read
|
|
272
|
+
* between requests. The bound is whatever the deadline has left, so the totals an operator set
|
|
273
|
+
* hold whatever the transport does. */
|
|
274
|
+
var res = await guard.async.bounded(
|
|
275
|
+
guard.async.invoked(state.transport,
|
|
276
|
+
[{ method: req.method, url: req.url, headers: req.headers, body: req.body, tls: state.tls,
|
|
277
|
+
timeout: timeout, maxResponseBytes: state.maxResponseBytes, blockPrivateAddresses: true,
|
|
278
|
+
allowPlaintextHttp: state.allowPlaintextHttp }],
|
|
279
|
+
E, "path/bad-input", "opts.transport"),
|
|
280
|
+
timeout, E, "path/revocation-fetch-failed", what);
|
|
281
|
+
res = res || {};
|
|
282
|
+
if (res.status !== 200) {
|
|
283
|
+
throw E("path/revocation-fetch-failed", what + " returned HTTP " + res.status + " (only 200 carries a revocation object; no redirect is followed)");
|
|
284
|
+
}
|
|
285
|
+
var body = guard.bytes.isByteSource(res.body)
|
|
286
|
+
? guard.bytes.source(res.body, PathError, "path/revocation-fetch-failed", "the revocation response body")
|
|
287
|
+
: Buffer.from(res.body == null ? "" : String(res.body), "latin1");
|
|
288
|
+
if (body.length === 0) throw E("path/revocation-fetch-failed", what + " returned an empty body");
|
|
289
|
+
if (body.length > state.maxResponseBytes) {
|
|
290
|
+
throw E("path/revocation-fetch-failed", what + " returned " + body.length + " bytes, over the " + state.maxResponseBytes + "-byte cap");
|
|
291
|
+
}
|
|
292
|
+
return { body: body, contentType: _contentType(res.headers) };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function _contentType(headers) {
|
|
296
|
+
var h = headers || {};
|
|
297
|
+
var keys = Object.keys(h);
|
|
298
|
+
for (var i = 0; i < keys.length; i++) {
|
|
299
|
+
if (keys[i].toLowerCase() !== "content-type") continue;
|
|
300
|
+
try { return mime.parse(String(h[keys[i]])).type; }
|
|
301
|
+
catch (_e) { return null; } // allow:swallow-unverified an unreadable media type is reported as absent, and it never selects a parser
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @internal One responder. The request is the RFC 5019 lightweight profile, so one Request, SHA-1
|
|
307
|
+
* CertID hashes and no singleRequestExtensions, and the HTTP shape is decided by sec. 5. The
|
|
308
|
+
* verdict is `pathValidate.verifyOcspResponse`, which is the single acceptance gate: the CertID
|
|
309
|
+
* match, the responder authorization including the delegate path, the signature and the currency
|
|
310
|
+
* all live there, and none of them is restated here. */
|
|
311
|
+
async function _tryOcsp(state, cert, issuer, ctx, candidate) {
|
|
312
|
+
var issuerCert = issuer.issuerCert;
|
|
313
|
+
if (!issuerCert) throw E("path/revocation-fetch-failed", "an OCSP request needs the issuer certificate, which this path does not carry");
|
|
314
|
+
var der = await ocsp.buildRequest({ cert: cert.bytes || cert, issuer: issuerCert.bytes || issuerCert },
|
|
315
|
+
{ profile: "lightweight", nonce: state.nonce });
|
|
316
|
+
var parsedReq = state.parseRequest(der);
|
|
317
|
+
var certId = parsedReq.requestList[0].certID;
|
|
318
|
+
var key = _ocspCacheKey(certId, candidate.url);
|
|
319
|
+
/** @internal A nonce asks this responder this question now, and every request carries a fresh
|
|
320
|
+
* one, so no response produced before this request can answer it. Reading the cache would hand
|
|
321
|
+
* back the previous response, whose nonce is the previous one, and the check below would refuse
|
|
322
|
+
* it: the cache would turn a working responder into a permanent undetermined. RFC 5019 sec. 6.1
|
|
323
|
+
* caches for the lightweight profile, which sec. 5 defines without a nonce, so the two are
|
|
324
|
+
* alternatives rather than a pair. */
|
|
325
|
+
var hit = state.nonce ? null : state.cache.get(key, ctx.time);
|
|
326
|
+
if (hit === null) {
|
|
327
|
+
var http = ocsp.httpRequest(der, candidate.url);
|
|
328
|
+
var got = await _fetchOne(state, http, "an OCSP request to " + candidate.url);
|
|
329
|
+
if (got.contentType !== null && got.contentType !== "application/ocsp-response") {
|
|
330
|
+
_append(state.notes, "the OCSP response carried content-type " + guard.text.showValue(got.contentType));
|
|
331
|
+
}
|
|
332
|
+
hit = got.body;
|
|
333
|
+
}
|
|
334
|
+
var v = await state.verifyOcspResponse(hit, cert, issuerCert, ctx.time,
|
|
335
|
+
{ historicalMode: ctx.historicalMode === true, requestNonce: state.nonce ? _requestNonce(parsedReq) : undefined });
|
|
336
|
+
/** @internal `valid` on that verdict means the certificate is good, not that the response was
|
|
337
|
+
* acceptable, so reading it as acceptance would turn a `revoked` answer into an undetermined
|
|
338
|
+
* one that `opts.softFail` could then waive. Acceptance is the three conditions the gate
|
|
339
|
+
* reports: the CertID matched, the signature verified, and the responder was authorized
|
|
340
|
+
* (RFC 6960 sec. 3.2). The status is then whatever the responder said. */
|
|
341
|
+
var accepted = !!v && v.matched === true && v.signatureValid === true && v.responderAuthorized === true;
|
|
342
|
+
if (!accepted) {
|
|
343
|
+
throw E("path/revocation-fetch-failed", "the OCSP response was not accepted: " + ((v && v.reason) || "no reason given"));
|
|
344
|
+
}
|
|
345
|
+
if (v.status !== "good" && v.status !== "revoked") {
|
|
346
|
+
throw E("path/revocation-fetch-failed", "the OCSP responder answered " + guard.text.showValue(v.status) +
|
|
347
|
+
((v && v.reason) ? ": " + v.reason : ""));
|
|
348
|
+
}
|
|
349
|
+
/** @internal RFC 5019 sec. 6.1 stores only an authoritative response, and sec. 4 refuses one with
|
|
350
|
+
* no nextUpdate, so the window an entry is reused within always exists by the time this runs. A
|
|
351
|
+
* nonce-bearing response is not stored, since nothing would ever read it back. */
|
|
352
|
+
if (!state.nonce && v.nextUpdate && v.thisUpdate) {
|
|
353
|
+
state.cache.put(key, hit, v.thisUpdate, v.nextUpdate, hit.length);
|
|
354
|
+
}
|
|
355
|
+
return { status: v.status, reason: "an OCSP response from " + candidate.url,
|
|
356
|
+
revocationReason: v.revocationReason, revocationTime: v.revocationTime };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @internal The bytes the nonce extension carries, which is what the response echoes. The
|
|
360
|
+
* extension's own `value` is those bytes inside their OCTET STRING, so comparing it would never
|
|
361
|
+
* match and the check would report every response as unechoed. */
|
|
362
|
+
function _requestNonce(parsedReq) {
|
|
363
|
+
var exts = parsedReq.requestExtensions || [];
|
|
364
|
+
for (var i = 0; i < exts.length; i++) {
|
|
365
|
+
if (exts[i].oid === OID_OCSP_NONCE) return exts[i].nonce || undefined;
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** @internal A cache entry stands in for a fetch for as long as the object's own nextUpdate says,
|
|
371
|
+
* so whatever is admitted is trusted until then and a forged list with a distant nextUpdate would
|
|
372
|
+
* silence the real one for that whole window. Admission is therefore decided about the object being
|
|
373
|
+
* kept: its signature has to verify under the key the path is working with, checked by the same
|
|
374
|
+
* verb `crlChecker` checks it with.
|
|
375
|
+
*
|
|
376
|
+
* The surrounding verdict cannot stand in for that. A base CRL and the delta it names are fetched
|
|
377
|
+
* in one pass and answered together, so a determinate answer there may have come from either, and
|
|
378
|
+
* the forged one would ride in on the genuine one's verdict. Scope and authorization are not
|
|
379
|
+
* decided here, because a cached object is handed back to `crlChecker` on every read and gated
|
|
380
|
+
* again; what the cache has to refuse is an object that is not the issuer's at all. */
|
|
381
|
+
function _keep(state, url, parsed, fetchedBytes, issuerNameBytes, keyBytes) {
|
|
382
|
+
if (!(fetchedBytes > 0) || !parsed.nextUpdate || !parsed.thisUpdate) return;
|
|
383
|
+
/** @internal Keyed on the name the CERTIFICATE carries, which is what the lookup above forms,
|
|
384
|
+
* and not on the name the CRL carries. The two are compared by the RFC 5280 sec. 7.1 canonical
|
|
385
|
+
* rule, so the same DN in another string encoding is accepted and still has different bytes:
|
|
386
|
+
* writing under the CRL's spelling would file the entry where nothing looks, and every
|
|
387
|
+
* validation would refetch inside a window the CRL had already signed for. */
|
|
388
|
+
state.crlCache.put(_crlCacheKey(url, issuerNameBytes, keyBytes), parsed, parsed.thisUpdate, parsed.nextUpdate, fetchedBytes);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** @internal Whether this CRL was signed by the key the path is working with, asked with the verb
|
|
392
|
+
* `crlChecker` asks it with. It answers only that one question: scope, the cRLSign authorization
|
|
393
|
+
* and currency are the checker's, and a CRL that fails any of those is still the issuer's. */
|
|
394
|
+
async function _isIssuers(state, parsed, issuer) {
|
|
395
|
+
try { return (await crlVerify.verifyCrlSignature(parsed, issuer.workingPublicKey)) === true; }
|
|
396
|
+
catch (e) {
|
|
397
|
+
_append(state.notes, "a fetched CRL could not be checked against the issuer's key: " + _say(e));
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** @internal `maxPerCert` bounds the destinations ONE certificate can send this client to, so the
|
|
403
|
+
* set it counts spans every route: a certificate naming five responders and five distribution
|
|
404
|
+
* points chose ten destinations, and counting each route's own list would let it reach twice the
|
|
405
|
+
* figure. A URL already counted is free to be asked again, which is what a retry is. */
|
|
406
|
+
function _takeUrl(state, url) {
|
|
407
|
+
if (state.urls.has(url)) return true;
|
|
408
|
+
if (state.urls.size >= state.maxPerCert) return false;
|
|
409
|
+
state.urls.add(url);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** @internal Every fetchable distribution point, until the CRLs gathered cover the certificate or
|
|
414
|
+
* the points run out. The acceptance rules are `crlChecker`'s: the issuer match, the cRLSign
|
|
415
|
+
* authorization, the IDP scope, the sec. 6.3.3(b) distribution point correspondence, the currency
|
|
416
|
+
* and the signature. This function decides which bytes to hand it and nothing else. */
|
|
417
|
+
async function _tryCrl(state, cert, issuer, ctx, candidates, certDeltas) {
|
|
418
|
+
/** @internal Counted and tried are different. `state.urls` counts the destinations one
|
|
419
|
+
* certificate names, across both routes, against `maxPerCert`; this set records what THIS walk
|
|
420
|
+
* has already asked, so a point named twice is asked once. A certificate naming one URL as its
|
|
421
|
+
* responder and as its distribution point is ordinary, and reading the counted set as the asked
|
|
422
|
+
* set drops the CRL route for it: the responder had counted the URL, so the fallback the OCSP
|
|
423
|
+
* route is documented to have never ran. */
|
|
424
|
+
var gathered = [], asked = new intrinsic.Set();
|
|
425
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
426
|
+
if (state.deadline.expired()) break;
|
|
427
|
+
var url = candidates[i].url;
|
|
428
|
+
if (asked.has(url)) continue;
|
|
429
|
+
if (!_takeUrl(state, url)) break;
|
|
430
|
+
asked.add(url);
|
|
431
|
+
/** @internal A CRL is reused inside the window it signs for, the same rule the responses
|
|
432
|
+
* follow: RFC 5280 sec. 6.3.3(a)(1)(ii) has a replacement checked for currency, so a cached
|
|
433
|
+
* one is kept only while `thisUpdate <= now < nextUpdate`. The key carries the issuer name the
|
|
434
|
+
* CRL itself declared, which is a different string from the URL the socket was opened on and
|
|
435
|
+
* from the encoded name the sec. 6.3.3(b) match reads. */
|
|
436
|
+
/** @internal One unreachable location must not lose the others: RFC 5280 sec. 4.2.1.13 has a
|
|
437
|
+
* certificate name several distribution points so a client can try them, and a point whose
|
|
438
|
+
* fetch or parse fails is noted and the walk continues. Only the exhausted list is a verdict. */
|
|
439
|
+
/** @internal The key carries the issuer the certificate names, not the URL alone. One URL
|
|
440
|
+
* serving several issuers' lists is an ordinary hosting arrangement, and a key that ignores
|
|
441
|
+
* the issuer hands the first certificate's list to the next certificate and suppresses its
|
|
442
|
+
* fetch, so which certificate was validated first decides the second one's verdict. */
|
|
443
|
+
var parsed = state.crlCache.get(_crlCacheKey(url, cert.issuer.bytes, issuer.workingPublicKey), ctx.time);
|
|
444
|
+
var fetchedBytes = 0;
|
|
445
|
+
if (parsed === null) {
|
|
446
|
+
try {
|
|
447
|
+
var got = await _fetchOne(state, { method: "GET", url: url, headers: { accept: "application/pkix-crl" } },
|
|
448
|
+
"a CRL fetch from " + url);
|
|
449
|
+
if (got.contentType !== null && got.contentType !== "application/pkix-crl") {
|
|
450
|
+
_append(state.notes, "the CRL from " + url + " carried content-type " + guard.text.showValue(got.contentType));
|
|
451
|
+
}
|
|
452
|
+
/** @internal RFC 5280 sec. 4.2.1.13 with RFC 2585 sec. 3: the body is exactly one DER
|
|
453
|
+
* CertificateList. One strict parser reads it, chosen by where the URL came from, so a PEM
|
|
454
|
+
* body, a bundle of two CRLs or a PKCS#7 wrapper is refused rather than unwrapped. */
|
|
455
|
+
parsed = crlSchema.parse(got.body);
|
|
456
|
+
fetchedBytes = got.body.length;
|
|
457
|
+
} catch (e) {
|
|
458
|
+
_append(state.notes, "a CRL fetch from " + url + " failed: " + _say(e));
|
|
459
|
+
if (state.fetches.count() >= state.maxFetches || state.deadline.expired()) break;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
_append(gathered, parsed);
|
|
464
|
+
/** @internal Whether this CRL is the issuer's, asked once and used twice: it decides whether
|
|
465
|
+
* the locations the CRL names are worth opening, and whether it may be kept. A CRL fetched
|
|
466
|
+
* over a plaintext hop can be replaced in transit, so a base that parses says nothing about
|
|
467
|
+
* who wrote it, and the `freshestCRL` of one that did not verify is the replacement's choice
|
|
468
|
+
* of destination rather than the issuer's. */
|
|
469
|
+
var authentic = await _isIssuers(state, parsed, issuer);
|
|
470
|
+
/** @internal RFC 5280 sec. 6.3.3(a) allows the delta location to be named by the certificate
|
|
471
|
+
* or by the base CRL, so both are read. The certificate's own freshestCRL is the certificate's
|
|
472
|
+
* to name, and the path already authenticated the certificate; the base CRL's is only followed
|
|
473
|
+
* once the base is the issuer's. A delta settles nothing on its own, which is why these are
|
|
474
|
+
* fetched beside a base rather than tried as candidates of their own. */
|
|
475
|
+
var deltaUrls = authentic ? _deltaUrlsFor(parsed, state) : [];
|
|
476
|
+
if (!authentic) _append(state.notes, "a CRL from " + url + " did not verify under the issuer's key, so the locations it names were not followed");
|
|
477
|
+
for (var cd = 0; cd < (certDeltas || []).length; cd++) _append(deltaUrls, certDeltas[cd].url);
|
|
478
|
+
for (var d = 0; d < deltaUrls.length && state.useDeltas; d++) {
|
|
479
|
+
if (asked.has(deltaUrls[d]) || state.deadline.expired()) continue;
|
|
480
|
+
if (!_takeUrl(state, deltaUrls[d])) continue;
|
|
481
|
+
asked.add(deltaUrls[d]);
|
|
482
|
+
try {
|
|
483
|
+
var dg = await _fetchOne(state, { method: "GET", url: deltaUrls[d], headers: { accept: "application/pkix-crl" } },
|
|
484
|
+
"a delta CRL fetch from " + deltaUrls[d]);
|
|
485
|
+
_append(gathered, crlSchema.parse(dg.body));
|
|
486
|
+
} catch (e) {
|
|
487
|
+
_append(state.notes, "a delta CRL fetch failed: " + _say(e));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
var interim = await state.crlChecker(gathered, { useDeltas: state.useDeltas }).check(cert, issuer, ctx);
|
|
491
|
+
if (interim && (interim.status === "good" || interim.status === "revoked")) {
|
|
492
|
+
if (authentic) _keep(state, url, parsed, fetchedBytes, cert.issuer.bytes, issuer.workingPublicKey);
|
|
493
|
+
return { status: interim.status, reason: "a CRL from " + url,
|
|
494
|
+
revocationReason: _reasonCodeOf(interim), revocationTime: interim.revocationTime };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (gathered.length === 0) throw E("path/revocation-fetch-failed", "no CRL was fetched");
|
|
498
|
+
var last = await state.crlChecker(gathered, { useDeltas: state.useDeltas }).check(cert, issuer, ctx);
|
|
499
|
+
return { status: (last && last.status) || "unknown", reason: (last && last.reason) || "the fetched CRLs did not settle the status",
|
|
500
|
+
revocationReason: _reasonCodeOf(last), revocationTime: last && last.revocationTime };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** @internal `crlChecker` names the CRLReason integer `reasonCode` and `verifyOcspResponse` names it
|
|
504
|
+
* `revocationReason`. This verb answers from either source, so it reads both and reports the one
|
|
505
|
+
* name. Reading only the OCSP name off a CRL verdict drops the reason from every CRL revocation. */
|
|
506
|
+
function _reasonCodeOf(verdict) {
|
|
507
|
+
if (verdict == null) return undefined;
|
|
508
|
+
var code = verdict.revocationReason;
|
|
509
|
+
if (typeof code !== "number") code = verdict.reasonCode;
|
|
510
|
+
return typeof code === "number" ? code : undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** @internal The delta locations a base CRL names in its own freshestCRL extension. The certificate's
|
|
514
|
+
* own freshestCRL is read beside it by the caller, so both sources RFC 5280 sec. 6.3.3(a) allows are
|
|
515
|
+
* covered. */
|
|
516
|
+
function _deltaUrlsFor(parsedCrl, state) {
|
|
517
|
+
var out = [];
|
|
518
|
+
var decoded = _softDecode(parsedCrl, OID_FRESHEST, state.notes);
|
|
519
|
+
if (decoded === null) return out;
|
|
520
|
+
var cands = _crlCandidates(decoded, state.net, state.notes);
|
|
521
|
+
for (var c = 0; c < cands.length; c++) _append(out, cands[c].url);
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* @primitive pki.path.fetchingChecker
|
|
527
|
+
* @signature pki.path.fetchingChecker(opts) -> { check }
|
|
528
|
+
* @since 0.8.8
|
|
529
|
+
* @status stable
|
|
530
|
+
* @spec RFC 5280, RFC 6960, RFC 5019
|
|
531
|
+
* @related pki.path.crlChecker, pki.path.ocspChecker, pki.ocsp.httpRequest
|
|
532
|
+
*
|
|
533
|
+
* A revocation checker that fetches what it needs, for `pki.path.validate`'s
|
|
534
|
+
* `opts.revocationChecker`. The shipped checkers are handed their CRLs and responses; this one
|
|
535
|
+
* reads the locations out of the certificate and asks for them over `opts.transport`.
|
|
536
|
+
*
|
|
537
|
+
* Fetching is off unless an operator builds this and passes it, which is how `opts.fetchAia`
|
|
538
|
+
* works: the default validation touches no network. It runs during `pki.path.validate` and not
|
|
539
|
+
* during `pki.path.build`, because RFC 5019 sec. 3.2 has a status check requested only after
|
|
540
|
+
* the chain's signatures are validated, and a build that fetched per candidate path would ask
|
|
541
|
+
* about certificates it has not yet accepted.
|
|
542
|
+
*
|
|
543
|
+
* OCSP is attempted first and a CRL after it, which RFC 5019 sec. 3.1 directs. The responder
|
|
544
|
+
* URL comes from an authorityInfoAccess AccessDescription whose accessMethod is id-ad-ocsp;
|
|
545
|
+
* the CRL URLs come from cRLDistributionPoints, and never from authorityInfoAccess, which
|
|
546
|
+
* RFC 5280 sec. 4.2.2.1 states outright. A distribution point is fetched only as a `fullName`
|
|
547
|
+
* carrying a URI whose scheme this client speaks: an ldap or ftp point, a
|
|
548
|
+
* nameRelativeToCRLIssuer, and a point naming a cRLIssuer and no location are each counted as
|
|
549
|
+
* a skip, so a certificate naming no fetchable point reads as that and never as a failed fetch.
|
|
550
|
+
*
|
|
551
|
+
* Nothing this module fetches is trusted by having been fetched. A CRL goes to
|
|
552
|
+
* `pki.path.crlChecker` and a response to `pki.path.verifyOcspResponse`, which are the one
|
|
553
|
+
* place each rule lives: the issuer match, the cRLSign authorization, the IDP scope, the
|
|
554
|
+
* sec. 6.3.3(b) correspondence, the CertID match, the responder authorization and both
|
|
555
|
+
* currency windows. The structure a body is parsed as is decided by which extension named the
|
|
556
|
+
* URL, so a Content-Type header is reported on a mismatch and never selects a parser.
|
|
557
|
+
*
|
|
558
|
+
* Every fault is an undetermined status carrying its reason, and `check` throws nothing: a
|
|
559
|
+
* transport fault, a non-200, an unparsable body, a stale object and a non-successful
|
|
560
|
+
* OCSPResponseStatus all land on `{ status: "unknown", reason }`. That is what keeps
|
|
561
|
+
* `opts.softFail` meaningful, since a checker that throws is not waivable. A configuration
|
|
562
|
+
* fault, such as a missing transport or a cap above the toolkit's own, throws here at
|
|
563
|
+
* construction.
|
|
564
|
+
*
|
|
565
|
+
* The verdict names the source that answered and lists the locations skipped and the faults
|
|
566
|
+
* noted in its `reason`; a `revoked` verdict adds `revocationReason` (the CRLReason integer)
|
|
567
|
+
* and `revocationTime` from whichever source decided it. `pki.path.validate` carries all three
|
|
568
|
+
* onto the certificate's `revocation` check row.
|
|
569
|
+
*
|
|
570
|
+
* Plaintext `http` is accepted by default, because a CRL and an OCSP response are signed
|
|
571
|
+
* objects whose signatures this toolkit verifies, and because an https-only revocation fetch
|
|
572
|
+
* needs the responder's own certificate validated first. `allowPlaintextHttp: false` narrows
|
|
573
|
+
* it to https. The SSRF guards do not move either way: no private, loopback or link-local
|
|
574
|
+
* destination, no redirect followed, a response-size cap, a per-certificate URL cap, a total
|
|
575
|
+
* fetch count and a wall-clock deadline across the whole validation.
|
|
576
|
+
*
|
|
577
|
+
* A URL taken from a certificate names a destination the certificate chose, so the two forms are
|
|
578
|
+
* decided in different places. An address literal is decided here and a private, loopback or
|
|
579
|
+
* link-local one is a skip. A name is decided by whatever resolves it, which is inside the
|
|
580
|
+
* transport, so a hostname candidate is a skip unless the transport declares
|
|
581
|
+
* `blocksPrivateAddresses === true` on itself, the flag the transport `pki.transport.https(defaults)`
|
|
582
|
+
* returns sets and backs by filtering the resolved address and pinning it for the connection.
|
|
583
|
+
* `pki.transport.https` is the factory rather than a transport, so pass what it returns. Without
|
|
584
|
+
* that declaration a certificate naming `crl.internal.example` would reach whatever the resolver
|
|
585
|
+
* returns.
|
|
586
|
+
*
|
|
587
|
+
* `maxPerCert` counts the distinct destinations one certificate sends this client to across both
|
|
588
|
+
* routes together, so a certificate naming five responders and five distribution points reaches
|
|
589
|
+
* the figure, not twice it. `maxFetches` and `totalDeadlineMs` bound a validation rather than a
|
|
590
|
+
* certificate: `pki.path.validate` calls a checker once per certificate and again for each anchor
|
|
591
|
+
* it tries, and all of those share one budget. A checker is reusable, each validation starting
|
|
592
|
+
* from the figures again; a `check` called directly is a validation of its own.
|
|
593
|
+
*
|
|
594
|
+
* A verified response is cached in process, keyed on the CertID and the responder URL, and a
|
|
595
|
+
* CRL on its request URL and the issuer name it carries. Reuse is decided from the signed
|
|
596
|
+
* thisUpdate and nextUpdate and from nothing else, which RFC 5019 sec. 7.5 directs; the HTTP
|
|
597
|
+
* caching headers are not read.
|
|
598
|
+
*
|
|
599
|
+
* An `ldap` distribution point is not fetched. That is a second protocol client rather than a
|
|
600
|
+
* flag, and it is re-opened when an operator presents a deployment whose only distribution
|
|
601
|
+
* point is one. An `ftp` point is a permanent non-goal.
|
|
602
|
+
*
|
|
603
|
+
* @opts transport REQUIRED. The `pki.transport` seam, or any `(request) -> Promise`
|
|
604
|
+
* of the same shape. A transport that does not declare that it blocks
|
|
605
|
+
* private addresses is refused a hostname.
|
|
606
|
+
* @opts allowPlaintextHttp Whether an `http` URL is fetched. Default `true`.
|
|
607
|
+
* @opts useDeltas Whether a freshestCRL delta is fetched and merged. Default `true`.
|
|
608
|
+
* @opts nonce Whether an OCSP request carries a 32-octet nonce. `true` or `false`,
|
|
609
|
+
* and anything else is `path/bad-input` rather than the default, since a
|
|
610
|
+
* control read as off is one an operator asked for and did not get.
|
|
611
|
+
* Default `false`, since a pre-produced response carries none and the
|
|
612
|
+
* currency window is the replay bound. Set, the response must echo the
|
|
613
|
+
* nonce or the status is undetermined and the route falls through to
|
|
614
|
+
* the CRL.
|
|
615
|
+
* @opts tls Passed to the transport for an https URL.
|
|
616
|
+
* @opts maxFetches Requests across one validation. Default `C.LIMITS.REVOCATION_MAX_FETCHES`.
|
|
617
|
+
* @opts maxPerCert Distinct destinations one certificate names, responders and distribution
|
|
618
|
+
* points counted together. Default `C.LIMITS.REVOCATION_MAX_PER_CERT`.
|
|
619
|
+
* @opts maxResponseBytes Default `C.LIMITS.REVOCATION_MAX_RESPONSE_BYTES`.
|
|
620
|
+
* @opts totalDeadlineMs Wall clock across one validation. Default `C.LIMITS.REVOCATION_TOTAL_DEADLINE_MS`.
|
|
621
|
+
* @opts sourceTimeoutMs Per request, narrowed to whatever the deadline has left; default
|
|
622
|
+
* `C.LIMITS.REVOCATION_SOURCE_TIMEOUT_MS`.
|
|
623
|
+
* @opts retries Attempts at the OCSP responder before the CRL is tried. Default `1`.
|
|
624
|
+
* @example
|
|
625
|
+
* // pki.transport.https is the factory; the transport is what it returns, and only that
|
|
626
|
+
* // carries the blocksPrivateAddresses declaration a hostname destination needs.
|
|
627
|
+
* var checker = pki.path.fetchingChecker({ transport: pki.transport.https({}) });
|
|
628
|
+
* console.log(typeof checker.check);
|
|
629
|
+
*/
|
|
630
|
+
function fetchingChecker(opts) {
|
|
631
|
+
opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "fetchingChecker: opts");
|
|
632
|
+
guard.identifier.assertKnownKeys(opts, CHECKER_OPTS, E, "path/bad-input",
|
|
633
|
+
"pki.path.fetchingChecker has an unknown option. The unknown option was: ");
|
|
634
|
+
/** @internal The checked value is the one used. Reading the option a second time would ask an
|
|
635
|
+
* accessor twice, so the transport that passed the door is the transport that gets called. */
|
|
636
|
+
var transport = guard.identifier.assertCallable(opts.transport, E, "path/bad-input",
|
|
637
|
+
"fetchingChecker: opts.transport", TRANSPORT_SHAPE);
|
|
638
|
+
var allowPlaintextHttp = opts.allowPlaintextHttp === undefined ? true : opts.allowPlaintextHttp === true;
|
|
639
|
+
var cfg = {
|
|
640
|
+
transport: transport,
|
|
641
|
+
/** @internal Read off the transport once, here, beside the transport itself: a flag read again
|
|
642
|
+
* at fetch time would be a second read of a caller's object, and the value that passed the
|
|
643
|
+
* door is the value the rule is applied on. */
|
|
644
|
+
net: intrinsic.freeze({ allowHttp: allowPlaintextHttp, guardsAddresses: _declaresAddressGuard(transport) }),
|
|
645
|
+
allowPlaintextHttp: allowPlaintextHttp,
|
|
646
|
+
useDeltas: !(opts.useDeltas === false),
|
|
647
|
+
nonce: _booleanOption(opts.nonce, "fetchingChecker: opts.nonce"),
|
|
648
|
+
tls: opts.tls,
|
|
649
|
+
maxFetches: guard.limits.cap(opts.maxFetches, "fetchingChecker: opts.maxFetches", LIMITS.REVOCATION_MAX_FETCHES,
|
|
650
|
+
{ E: E, code: "path/bad-input", min: 1, max: LIMITS.REVOCATION_MAX_FETCHES }),
|
|
651
|
+
maxPerCert: guard.limits.cap(opts.maxPerCert, "fetchingChecker: opts.maxPerCert", LIMITS.REVOCATION_MAX_PER_CERT,
|
|
652
|
+
{ E: E, code: "path/bad-input", min: 1, max: LIMITS.REVOCATION_MAX_PER_CERT }),
|
|
653
|
+
maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "fetchingChecker: opts.maxResponseBytes", LIMITS.REVOCATION_MAX_RESPONSE_BYTES,
|
|
654
|
+
{ E: E, code: "path/bad-input", min: 1, max: LIMITS.REVOCATION_MAX_RESPONSE_BYTES }),
|
|
655
|
+
totalDeadlineMs: guard.limits.cap(opts.totalDeadlineMs, "fetchingChecker: opts.totalDeadlineMs", LIMITS.REVOCATION_TOTAL_DEADLINE_MS,
|
|
656
|
+
{ E: E, code: "path/bad-input", min: 1, max: LIMITS.REVOCATION_TOTAL_DEADLINE_MS }),
|
|
657
|
+
sourceTimeoutMs: guard.limits.cap(opts.sourceTimeoutMs, "fetchingChecker: opts.sourceTimeoutMs", LIMITS.REVOCATION_SOURCE_TIMEOUT_MS,
|
|
658
|
+
{ E: E, code: "path/bad-input", min: 1, max: LIMITS.REVOCATION_SOURCE_TIMEOUT_MS }),
|
|
659
|
+
retries: guard.limits.cap(opts.retries, "fetchingChecker: opts.retries", 1, { E: E, code: "path/bad-input", min: 0, max: 4 }),
|
|
660
|
+
crlChecker: pathValidate.crlChecker,
|
|
661
|
+
verifyOcspResponse: pathValidate.verifyOcspResponse,
|
|
662
|
+
parseRequest: ocspSchema.parseRequest,
|
|
663
|
+
};
|
|
664
|
+
var cache = _makeCache(LIMITS.REVOCATION_CACHE_MAX_ENTRIES, LIMITS.REVOCATION_CACHE_MAX_BYTES);
|
|
665
|
+
var crlCache = _makeCache(LIMITS.REVOCATION_CACHE_MAX_ENTRIES, LIMITS.REVOCATION_CACHE_MAX_BYTES);
|
|
666
|
+
/** @internal `maxFetches` and `totalDeadlineMs` bound a validation, and a validation is several
|
|
667
|
+
* `check` calls: one per certificate on the path, and the set again for each anchor tried. A
|
|
668
|
+
* budget minted per call would be those figures multiplied by the path length, which is a bound
|
|
669
|
+
* the operator did not set. `pki.path.validate` names its run and the budget is held against that
|
|
670
|
+
* name, so the certificates of one path share it. The map is weak, so the entry goes when the
|
|
671
|
+
* validation does and a checker is reusable at its full figure on the next one. A `check` called
|
|
672
|
+
* directly, with no run named, is its own validation and gets its own budget. */
|
|
673
|
+
var budgets = new intrinsic.WeakMap();
|
|
674
|
+
function budgetFor(run) {
|
|
675
|
+
var fresh = {
|
|
676
|
+
deadline: guard.limits.deadline(cfg.totalDeadlineMs),
|
|
677
|
+
fetches: guard.limits.counter(cfg.maxFetches, E, "path/revocation-fetch-failed", "revocation fetch"),
|
|
678
|
+
};
|
|
679
|
+
if (run === null || typeof run !== "object") return fresh;
|
|
680
|
+
var held = intrinsic.weakGet(budgets, run);
|
|
681
|
+
if (held !== undefined) return held;
|
|
682
|
+
intrinsic.weakSet(budgets, run, fresh);
|
|
683
|
+
return fresh;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
check: async function (cert, issuer, ctx) {
|
|
688
|
+
var notes = [], skips = [];
|
|
689
|
+
var budget = budgetFor(ctx == null ? null : ctx.run);
|
|
690
|
+
var state = intrinsic.assign(intrinsic.create(null), cfg, {
|
|
691
|
+
notes: notes, cache: cache, crlCache: crlCache,
|
|
692
|
+
deadline: budget.deadline,
|
|
693
|
+
fetches: budget.fetches,
|
|
694
|
+
urls: new intrinsic.Set(),
|
|
695
|
+
});
|
|
696
|
+
var ocspCands, crlCands, certDeltas;
|
|
697
|
+
try {
|
|
698
|
+
ocspCands = _ocspCandidates(_softDecode(cert, OID_AIA, skips), cfg.net, skips);
|
|
699
|
+
crlCands = _crlCandidates(_softDecode(cert, OID_CDP, skips), cfg.net, skips);
|
|
700
|
+
var fresh = _softDecode(cert, OID_FRESHEST, skips);
|
|
701
|
+
certDeltas = (fresh && cfg.useDeltas) ? _crlCandidates(fresh, cfg.net, skips) : [];
|
|
702
|
+
} catch (e) {
|
|
703
|
+
return _unknown("the revocation locations could not be read: " + _say(e), notes, skips);
|
|
704
|
+
}
|
|
705
|
+
/** @internal A certificate may name a delta location and no base one. `crlChecker` treats an
|
|
706
|
+
* unmerged delta that lists the serial as authoritative, so such a certificate has a real
|
|
707
|
+
* revoked answer available and returning undetermined without asking would lose it. The
|
|
708
|
+
* delta is tried as a candidate of its own here, which is the only place it can be: below,
|
|
709
|
+
* the deltas are fetched beside a base that answered. */
|
|
710
|
+
if (crlCands.length === 0 && certDeltas.length > 0) crlCands = certDeltas;
|
|
711
|
+
if (ocspCands.length === 0 && crlCands.length === 0) {
|
|
712
|
+
return _unknown("the certificate names no revocation location this client fetches", notes, skips);
|
|
713
|
+
}
|
|
714
|
+
/** @internal RFC 5019 sec. 3.1: the responder first, the CRL after a configured timeout and
|
|
715
|
+
* number of retries. A responder that answers `revoked` or `good` ends it; anything else
|
|
716
|
+
* falls through, because sec. 5 of RFC 6960 makes the CRL the fallback and never an
|
|
717
|
+
* assumption of good standing. */
|
|
718
|
+
var lastReason = null;
|
|
719
|
+
for (var a = 0; a <= cfg.retries && ocspCands.length > 0; a++) {
|
|
720
|
+
for (var o = 0; o < ocspCands.length; o++) {
|
|
721
|
+
if (!_takeUrl(state, ocspCands[o].url)) {
|
|
722
|
+
_append(skips, "a responder beyond the maxPerCert destinations this certificate may name");
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
var got = await _tryOcsp(state, cert, issuer, ctx, ocspCands[o]);
|
|
727
|
+
if (got.status === "good" || got.status === "revoked") return _verdict(got, notes, skips);
|
|
728
|
+
lastReason = got.reason + " reported " + got.status;
|
|
729
|
+
} catch (e) { lastReason = _say(e); }
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (crlCands.length > 0) {
|
|
733
|
+
try {
|
|
734
|
+
var viaCrl = await _tryCrl(state, cert, issuer, ctx, crlCands, certDeltas);
|
|
735
|
+
if (viaCrl.status === "good" || viaCrl.status === "revoked") return _verdict(viaCrl, notes, skips);
|
|
736
|
+
lastReason = viaCrl.reason;
|
|
737
|
+
} catch (e2) { lastReason = _say(e2); }
|
|
738
|
+
}
|
|
739
|
+
return _unknown(lastReason || "no revocation source answered", notes, skips);
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** @internal Every reason this module reports about a fault it did not cause passes through here,
|
|
745
|
+
* and the value is the transport's: `check` promises a verdict rather than a throw, and a value
|
|
746
|
+
* whose `message` refuses to be read would break that promise at the one place that formats it.
|
|
747
|
+
* @guard-via guard.text.describeThrown */
|
|
748
|
+
function _say(e) { return guard.text.describeThrown(e); }
|
|
749
|
+
|
|
750
|
+
function _verdict(got, notes, skips) {
|
|
751
|
+
return guard.verdict.of({
|
|
752
|
+
status: got.status, reason: _reason(got.reason, notes, skips),
|
|
753
|
+
revocationReason: got.revocationReason, revocationTime: got.revocationTime,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function _unknown(reason, notes, skips) {
|
|
758
|
+
return guard.verdict.of({ status: "unknown", reason: _reason(reason, notes, skips) });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** @internal A skip and a failure are carried separately into the one reason string, so an operator
|
|
762
|
+
* reading an undetermined verdict can tell a blocked port from a certificate that names nothing
|
|
763
|
+
* fetchable. The verdict itself is the same either way, which is why this is a reason and not a
|
|
764
|
+
* second error code. */
|
|
765
|
+
function _reason(head, notes, skips) {
|
|
766
|
+
var parts = [head];
|
|
767
|
+
if (skips.length) _append(parts, "skipped: " + skips.join("; "));
|
|
768
|
+
if (notes.length) _append(parts, "noted: " + notes.join("; "));
|
|
769
|
+
return parts.join(" | ");
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
module.exports = { fetchingChecker: fetchingChecker };
|