@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
package/lib/path-validate.js
CHANGED
|
@@ -34,6 +34,7 @@ var webcrypto = require("./webcrypto");
|
|
|
34
34
|
var pkix = require("./schema-pkix");
|
|
35
35
|
var oid = require("./oid");
|
|
36
36
|
var ct = require("./ct");
|
|
37
|
+
var identity = require("./identity-match");
|
|
37
38
|
var errors = require("./framework-error");
|
|
38
39
|
var asn1 = require("./asn1-der");
|
|
39
40
|
var schema = require("./schema-engine");
|
|
@@ -493,35 +494,17 @@ function hostConstraintMatch(constraint, host, reachIntoBase) {
|
|
|
493
494
|
return false;
|
|
494
495
|
}
|
|
495
496
|
|
|
496
|
-
function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
|
|
497
497
|
function _startsWithDigit(s) { if (s.length === 0) return false; var c = _charCodeAt(s, 0); return c >= 48 && c <= 57; }
|
|
498
498
|
|
|
499
|
-
var
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
var c0 = _charCodeAt(uri, 0);
|
|
504
|
-
if (!((c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122))) return null;
|
|
505
|
-
var p = 1;
|
|
506
|
-
while (p < n && _SCHEME_TABLE[_charCodeAt(uri, p)]) p += 1;
|
|
507
|
-
if (_charCodeAt(uri, p) !== 58 || _charCodeAt(uri, p + 1) !== 47 || _charCodeAt(uri, p + 2) !== 47) return null;
|
|
508
|
-
p += 3;
|
|
509
|
-
var authStart = p;
|
|
510
|
-
while (p < n) { var d = _charCodeAt(uri, p); if (d === 47 || d === 63 || d === 35) break; p += 1; }
|
|
511
|
-
return [authStart, p];
|
|
512
|
-
}
|
|
513
|
-
function _portStripIndex(s) {
|
|
514
|
-
var i = s.length;
|
|
515
|
-
while (i > 0) { var c = _charCodeAt(s, i - 1); if (c >= 48 && c <= 57) i -= 1; else break; }
|
|
516
|
-
return (i < s.length && i > 0 && _charCodeAt(s, i - 1) === 58) ? i - 1 : s.length;
|
|
517
|
-
}
|
|
499
|
+
var isFqdnHost = guard.name.isFqdnHost;
|
|
500
|
+
|
|
501
|
+
/** @internal `URL.hostname` keeps an IPv6 literal's brackets, and `net.isIP` reads the address
|
|
502
|
+
* without them. */
|
|
518
503
|
function _stripBrackets(s) {
|
|
519
504
|
var n = s.length;
|
|
520
505
|
return (n >= 2 && _charCodeAt(s, 0) === 91 && _charCodeAt(s, n - 1) === 93) ? s.slice(1, n - 1) : s;
|
|
521
506
|
}
|
|
522
507
|
|
|
523
|
-
var isFqdnHost = guard.name.isFqdnHost;
|
|
524
|
-
|
|
525
508
|
function uriMatch(constraint, uri) {
|
|
526
509
|
var host = uriHost(uri);
|
|
527
510
|
if (host === null) return "unsupported";
|
|
@@ -532,15 +515,14 @@ function uriMatch(constraint, uri) {
|
|
|
532
515
|
return h === c;
|
|
533
516
|
}
|
|
534
517
|
|
|
518
|
+
/** @internal The host a `uniformResourceIdentifier` constraint compares against. A URI written
|
|
519
|
+
* without an authority has no host, and RFC 5280 sec. 4.2.1.10 constrains a host, so the caller
|
|
520
|
+
* reads that as `unsupported` rather than as a name. The brackets an IPv6 literal arrives in are
|
|
521
|
+
* kept, so the value stays the one the certificate carries and `isFqdnHost` refuses it. */
|
|
535
522
|
function uriHost(uri) {
|
|
536
|
-
var
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
var firstAt = authority.indexOf("@");
|
|
540
|
-
if (firstAt !== authority.lastIndexOf("@")) return null;
|
|
541
|
-
if (firstAt !== -1) authority = authority.slice(firstAt + 1);
|
|
542
|
-
var host = authority.slice(0, _portStripIndex(authority));
|
|
543
|
-
return host === "" ? null : host;
|
|
523
|
+
var p = guard.name.uriParts(uri);
|
|
524
|
+
if (p === null || !p.hasAuthority || p.host === null) return null;
|
|
525
|
+
return p.bracketed ? "[" + p.host + "]" : p.host;
|
|
544
526
|
}
|
|
545
527
|
|
|
546
528
|
function ipMatch(constraint, addr) {
|
|
@@ -995,6 +977,25 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
995
977
|
return null;
|
|
996
978
|
}
|
|
997
979
|
|
|
980
|
+
/** @internal A checker decides on more than a status: which source answered, which locations it
|
|
981
|
+
* skipped, and for a revocation the CRLReason and the time the entry named. Those reach the caller
|
|
982
|
+
* on the check row, because the status alone cannot tell a waiver what it waived or a revocation
|
|
983
|
+
* why. The verdict is a caller-supplied object where `opts.revocationChecker` is, so each field is
|
|
984
|
+
* read once into a local and the value is copied rather than the object. `reasonCode` is the name
|
|
985
|
+
* `crlChecker` gives the CRLReason integer and `revocationReason` the name `ocspChecker` gives it;
|
|
986
|
+
* both are read so the row does not depend on which checker answered. */
|
|
987
|
+
function _revocationRow(row, rv) {
|
|
988
|
+
if (rv == null) return row;
|
|
989
|
+
var reason = rv.reason;
|
|
990
|
+
if (typeof reason === "string" && reason.length > 0) row.reason = reason;
|
|
991
|
+
var code = rv.revocationReason;
|
|
992
|
+
if (typeof code !== "number") code = rv.reasonCode;
|
|
993
|
+
if (intrinsic.numberIsFinite(code)) row.revocationReason = code;
|
|
994
|
+
var at = rv.revocationTime;
|
|
995
|
+
if (guard.time.isDate(at)) row.revocationTime = new intrinsic.Date(guard.time.instantOf(at));
|
|
996
|
+
return row;
|
|
997
|
+
}
|
|
998
|
+
|
|
998
999
|
/**
|
|
999
1000
|
* @primitive pki.path.validate
|
|
1000
1001
|
* @signature pki.path.validate(path, opts) -> Promise<result>
|
|
@@ -1060,7 +1061,10 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
1060
1061
|
* one could not be answered at all and the path fails for it. The
|
|
1061
1062
|
* per-certificate `revocation` check carries the `status` it was decided on and
|
|
1062
1063
|
* marks a waiver, so "checked, good" is distinguishable from "could not check,
|
|
1063
|
-
* and you waived it", a distinction a stored verdict is re-read to settle.
|
|
1064
|
+
* and you waived it", a distinction a stored verdict is re-read to settle. It
|
|
1065
|
+
* also carries what the checker decided on: `reason` (the source that answered,
|
|
1066
|
+
* the locations it skipped, the faults it noted), and for a revocation
|
|
1067
|
+
* `revocationReason` (the CRLReason integer) and `revocationTime`. A
|
|
1064
1068
|
* throw from a checker is a fault in the checker: no status was reported, so the
|
|
1065
1069
|
* path fails as `path/revocation-checker-error` carrying the fault whatever
|
|
1066
1070
|
* `softFail` says. `softFail` opts into an undetermined answer; the built-in
|
|
@@ -1105,7 +1109,7 @@ function validateCriticalExtensionStructure(cert) {
|
|
|
1105
1109
|
* res.valid; // boolean; res.results[0].checks carries the per-check codes
|
|
1106
1110
|
*/
|
|
1107
1111
|
var _VALIDATE_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1108
|
-
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, initialAnyPolicyInhibit: 1,
|
|
1112
|
+
checkPurpose: 1, ctLogList: 1, ctPolicy: 1, historicalMode: 1, identity: 1, initialAnyPolicyInhibit: 1,
|
|
1109
1113
|
initialExcludedSubtrees: 1, initialExplicitPolicy: 1, initialPermittedSubtrees: 1,
|
|
1110
1114
|
initialPolicyMappingInhibit: 1, maxPathCerts: 1, maxPolicyNodes: 1, minRsaModulusBits: 1, requireRevocation: 1,
|
|
1111
1115
|
requiredEku: 1, revocationChecker: 1, softFail: 1, time: 1, trustAnchors: 1,
|
|
@@ -1139,9 +1143,22 @@ async function ctGateCheck(leaf, certs, anchor, ctLogList, ctPolicy, n, at) {
|
|
|
1139
1143
|
}
|
|
1140
1144
|
|
|
1141
1145
|
async function validate(path, opts) {
|
|
1142
|
-
|
|
1143
|
-
guard.identifier.assertKnownKeys(
|
|
1146
|
+
var checked = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
|
|
1147
|
+
guard.identifier.assertKnownKeys(checked, _VALIDATE_OPTS, E, "path/bad-input",
|
|
1144
1148
|
"pki.path.validate has an unknown option (the anchor option is `trustAnchors`, a single anchor or an array). The unknown option was: ");
|
|
1149
|
+
/** @internal One validation, named by an object nothing outside this module can reach. A
|
|
1150
|
+
* revocation checker that budgets fetches is called once per certificate and again per anchor
|
|
1151
|
+
* attempt, so without a name for the run it cannot tell "the next certificate on this path" from
|
|
1152
|
+
* "a new validation", and the totals it documents become per-certificate totals. The checker
|
|
1153
|
+
* holds its state against this object; the object is unreachable once the validation returns, so
|
|
1154
|
+
* a checker stays reusable and nothing accumulates between runs. The anchor recursion carries the
|
|
1155
|
+
* same one, and a caller has no way to supply one, so two validations never share a budget. */
|
|
1156
|
+
return _validate(path, checked, intrinsic.freeze(intrinsic.create(null)));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** @internal The options are the ones the door above checked, and each anchor attempt is handed a
|
|
1160
|
+
* copy this module built, so the door runs once per call rather than once per anchor. */
|
|
1161
|
+
async function _validate(path, opts, run) {
|
|
1145
1162
|
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)");
|
|
1146
1163
|
if (opts.ctLogList != null) {
|
|
1147
1164
|
if (typeof opts.ctLogList !== "object" || opts.ctLogList.byLogId == null) throw E("path/bad-input", "validate: opts.ctLogList must be a pki.ct.parseLogList result");
|
|
@@ -1150,6 +1167,15 @@ async function validate(path, opts) {
|
|
|
1150
1167
|
if (ctp.minScts != null && (typeof ctp.minScts !== "number" || !intrinsic.isInteger(ctp.minScts) || ctp.minScts < 1)) throw E("path/bad-input", "validate: opts.ctPolicy.minScts must be a positive integer");
|
|
1151
1168
|
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");
|
|
1152
1169
|
}
|
|
1170
|
+
/** @internal RFC 9525 sec. 6.1.1 has the client build its reference identities before it asks,
|
|
1171
|
+
* and everything between this call and the comparison is awaited, so the list is read and copied
|
|
1172
|
+
* here. A caller that reuses its options object, or whose object is reachable from elsewhere,
|
|
1173
|
+
* then cannot change the question after asking it. */
|
|
1174
|
+
var identityRefs = null;
|
|
1175
|
+
if (opts.identity != null) {
|
|
1176
|
+
try { identityRefs = identity.snapshotReferences(opts.identity); }
|
|
1177
|
+
catch (e) { throw E("path/bad-input", "validate: opts.identity is not a reference identity this toolkit reads", e); }
|
|
1178
|
+
}
|
|
1153
1179
|
if (!intrinsic.isArray(path)) throw E("path/bad-input", "validate: path must be an array of certificates");
|
|
1154
1180
|
var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
1155
1181
|
if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
|
|
@@ -1193,17 +1219,21 @@ async function validate(path, opts) {
|
|
|
1193
1219
|
o.trustAnchors = a;
|
|
1194
1220
|
if (permRawOnce != null) o.initialPermittedSubtrees = callerPermitted;
|
|
1195
1221
|
if (exclRawOnce != null) o.initialExcludedSubtrees = callerExcluded;
|
|
1222
|
+
/** @internal The snapshot taken in this call's prologue, so every anchor attempt asks the
|
|
1223
|
+
* question the caller asked. Copying `opts.identity` across would have each attempt read
|
|
1224
|
+
* the caller's own list again, after the awaits the earlier attempts spent. */
|
|
1225
|
+
if (identityRefs !== null) o.identity = identityRefs;
|
|
1196
1226
|
return o;
|
|
1197
1227
|
};
|
|
1198
1228
|
var lastMatched = null, anyMatched = false;
|
|
1199
1229
|
for (var ai = 0; ai < tuples.length; ai++) {
|
|
1200
1230
|
if (!dnEqual(tuples[ai].name.rdns, certs[0].issuer.rdns)) continue;
|
|
1201
1231
|
anyMatched = true;
|
|
1202
|
-
var oneRes = await
|
|
1232
|
+
var oneRes = await _validate(path, singleOpts(tuples[ai]), run);
|
|
1203
1233
|
if (oneRes && oneRes.valid) return oneRes;
|
|
1204
1234
|
lastMatched = oneRes;
|
|
1205
1235
|
}
|
|
1206
|
-
return anyMatched ? lastMatched :
|
|
1236
|
+
return anyMatched ? lastMatched : _validate(path, singleOpts(tuples[0]), run);
|
|
1207
1237
|
}
|
|
1208
1238
|
var anchor = toAnchor(anchorList[0], "validate");
|
|
1209
1239
|
guard.time.assertValid(opts.time, E, "path/bad-input", "validate: opts.time (the always-on validity-window check date)");
|
|
@@ -1228,6 +1258,7 @@ async function validate(path, opts) {
|
|
|
1228
1258
|
var requireRevocation = opts.requireRevocation === true;
|
|
1229
1259
|
var failed = false;
|
|
1230
1260
|
var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
|
|
1261
|
+
var prefixAuthentic = true;
|
|
1231
1262
|
var anchorDistrustApplied = false, anchorPurposeApplied = false;
|
|
1232
1263
|
if (!checkPurpose && _hasPurposeScopedMetadata(anchor)) {
|
|
1233
1264
|
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");
|
|
@@ -1258,6 +1289,11 @@ async function validate(path, opts) {
|
|
|
1258
1289
|
}
|
|
1259
1290
|
guard.list.append(checks, { name: "signature", ok: sigRes.ok, code: sigRes.ok ? undefined : (sigRes.code || "path/bad-signature") });
|
|
1260
1291
|
if (!sigRes.ok) failed = true;
|
|
1292
|
+
/** @internal What authenticates a certificate is the chain above it, not its own signature:
|
|
1293
|
+
* a forged issuer fails its own check and its key still becomes the working key, so everything
|
|
1294
|
+
* below verifies against a key nothing vouched for. Once a signature on the path has failed,
|
|
1295
|
+
* no certificate under it is authenticated however its own check reads. */
|
|
1296
|
+
if (!sigRes.ok) prefixAuthentic = false;
|
|
1261
1297
|
|
|
1262
1298
|
if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1263
1299
|
var cku = compositeKeyUsageCheck(cert);
|
|
@@ -1298,24 +1334,38 @@ async function validate(path, opts) {
|
|
|
1298
1334
|
if (!ncRes.ok) failed = true;
|
|
1299
1335
|
}
|
|
1300
1336
|
|
|
1301
|
-
|
|
1337
|
+
/** @internal RFC 5019 sec. 3.2 has the status asked for after the chain signatures are
|
|
1338
|
+
* validated, and a certificate no chain has authenticated is a claim: the serial and issuer a
|
|
1339
|
+
* responder would answer about are ones nothing has bound to a key. A checker may open a
|
|
1340
|
+
* socket, so asking anyway lets an unauthenticated certificate choose a destination and spend
|
|
1341
|
+
* the fetch budget. The row says the status was not asked for, which is an undetermined
|
|
1342
|
+
* outcome and not a checked one. */
|
|
1343
|
+
if (revocationChecker && !prefixAuthentic) {
|
|
1344
|
+
guard.list.append(checks, { name: "revocation", ok: false, status: "unknown",
|
|
1345
|
+
code: "path/revocation-undetermined",
|
|
1346
|
+
reason: sigRes.ok
|
|
1347
|
+
? "a signature above this certificate on the path did not verify, so no revocation status was asked for"
|
|
1348
|
+
: "the certificate's own signature did not verify, so no revocation status was asked for" });
|
|
1349
|
+
revocationUndetermined = true;
|
|
1350
|
+
revocationRan = true;
|
|
1351
|
+
} else if (revocationChecker) {
|
|
1302
1352
|
var issuerCert = idx > 0 ? certs[idx - 1] : null;
|
|
1303
1353
|
var rv, rvError = null;
|
|
1304
|
-
try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true }); }
|
|
1354
|
+
try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true, run: run }); }
|
|
1305
1355
|
catch (e) { rv = { status: "error" }; rvError = e; }
|
|
1306
1356
|
var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
|
|
1307
|
-
if (rv && rv.status === "good") { guard.list.append(checks,{ name: "revocation", ok: true, status: "good" }); }
|
|
1308
|
-
else if (rv && rv.status === "revoked") { guard.list.append(checks,{ name: "revocation", ok: false, status: "revoked", code: "path/revoked" }); failed = true; }
|
|
1357
|
+
if (rv && rv.status === "good") { guard.list.append(checks, _revocationRow({ name: "revocation", ok: true, status: "good" }, rv)); }
|
|
1358
|
+
else if (rv && rv.status === "revoked") { guard.list.append(checks, _revocationRow({ name: "revocation", ok: false, status: "revoked", code: "path/revoked" }, rv)); failed = true; }
|
|
1309
1359
|
else if (rvError) {
|
|
1310
1360
|
guard.list.append(checks,{ name: "revocation", ok: false, status: "error", code: "path/revocation-checker-error", error: rvError });
|
|
1311
1361
|
revocationUndetermined = true;
|
|
1312
1362
|
failed = true;
|
|
1313
1363
|
}
|
|
1314
1364
|
else if (softFail) {
|
|
1315
|
-
guard.list.append(checks,{ name: "revocation", ok: true, status: rvStatus, waived: true });
|
|
1365
|
+
guard.list.append(checks, _revocationRow({ name: "revocation", ok: true, status: rvStatus, waived: true }, rv));
|
|
1316
1366
|
revocationWaived = true;
|
|
1317
1367
|
} else {
|
|
1318
|
-
guard.list.append(checks,{ name: "revocation", ok: false, status: rvStatus, code: "path/revocation-undetermined" });
|
|
1368
|
+
guard.list.append(checks, _revocationRow({ name: "revocation", ok: false, status: rvStatus, code: "path/revocation-undetermined" }, rv));
|
|
1319
1369
|
revocationUndetermined = true;
|
|
1320
1370
|
failed = true;
|
|
1321
1371
|
}
|
|
@@ -1404,8 +1454,26 @@ async function validate(path, opts) {
|
|
|
1404
1454
|
if (!ctCheck.ok) failed = true;
|
|
1405
1455
|
}
|
|
1406
1456
|
|
|
1457
|
+
/** @internal RFC 9525 sec. 1.2 has identity matching run in addition to the sec. 6.1 checks
|
|
1458
|
+
* rather than in place of any of them, and sec. 1.2 again has it read the leaf alone. So this
|
|
1459
|
+
* runs after the per-certificate loop, against `certs[n-1]`, and its row joins that certificate's
|
|
1460
|
+
* checks beside the sec. 6.1 rows rather than replacing them. */
|
|
1461
|
+
var identityChecked = false;
|
|
1462
|
+
if (identityRefs !== null) {
|
|
1463
|
+
var idVerdict = identity.match(certs[n - 1], identityRefs);
|
|
1464
|
+
var idRow = guard.verdict.of(idVerdict.matched
|
|
1465
|
+
? { name: "identity", ok: true }
|
|
1466
|
+
: { name: "identity", ok: false, code: "path/identity-mismatch" });
|
|
1467
|
+
var idTarget = null;
|
|
1468
|
+
for (var ir = 0; ir < state.results.length; ir++) { if (state.results[ir].index === n - 1) { idTarget = state.results[ir]; break; } }
|
|
1469
|
+
if (idTarget) guard.list.append(idTarget.checks, idRow);
|
|
1470
|
+
if (!idVerdict.matched) failed = true;
|
|
1471
|
+
identityChecked = idVerdict.matched ? idVerdict.matchedReference : false;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1407
1474
|
return guard.verdict.of({
|
|
1408
1475
|
valid: !failed,
|
|
1476
|
+
identityChecked: identityChecked,
|
|
1409
1477
|
revocationChecked: !revocationRan ? false
|
|
1410
1478
|
: revocationUndetermined ? "undetermined"
|
|
1411
1479
|
: revocationWaived ? "waived" : "determined",
|
|
@@ -1621,7 +1689,8 @@ function selectDelta(candidates) {
|
|
|
1621
1689
|
* `unknown`, which the validator fails closed unless `softFail` is set.
|
|
1622
1690
|
*
|
|
1623
1691
|
* A `revoked` verdict carries `reasonCode` (the CRLReason integer, 0 for
|
|
1624
|
-
* `unspecified`)
|
|
1692
|
+
* `unspecified`), a `reason` naming it, and `revocationTime` from the entry's
|
|
1693
|
+
* revocationDate.
|
|
1625
1694
|
*
|
|
1626
1695
|
* @opts
|
|
1627
1696
|
* useDeltas boolean merge delta CRLs onto their base (RFC 5280 sec. 6.3.1(b)).
|
|
@@ -1665,7 +1734,7 @@ function crlChecker(crls, opts) {
|
|
|
1665
1734
|
if (iku && iku.value.cRLSign !== true) signerAuthorized = false;
|
|
1666
1735
|
}
|
|
1667
1736
|
|
|
1668
|
-
var certStatus = null;
|
|
1737
|
+
var certStatus = null, certTime = null;
|
|
1669
1738
|
var reasonsMask = 0;
|
|
1670
1739
|
var releasedByUnmergedDelta = false;
|
|
1671
1740
|
var sawUnmergedDelta = false;
|
|
@@ -1743,7 +1812,8 @@ function crlChecker(crls, opts) {
|
|
|
1743
1812
|
if (historical && guard.time.isDate(entry.revocationDate) &&
|
|
1744
1813
|
guard.time.instantOf(entry.revocationDate) > guard.time.instantOf(time)) continue;
|
|
1745
1814
|
var rc = crlEntryReason(entry);
|
|
1746
|
-
return rc === null ? 0 : rc
|
|
1815
|
+
return { code: rc === null ? 0 : rc,
|
|
1816
|
+
at: guard.time.isDate(entry.revocationDate) ? entry.revocationDate : null };
|
|
1747
1817
|
}
|
|
1748
1818
|
return null;
|
|
1749
1819
|
}
|
|
@@ -1779,9 +1849,9 @@ function crlChecker(crls, opts) {
|
|
|
1779
1849
|
} else {
|
|
1780
1850
|
status = scanCrl(rec.crl);
|
|
1781
1851
|
}
|
|
1782
|
-
if (status === 8) status = null;
|
|
1852
|
+
if (status !== null && status.code === 8) status = null;
|
|
1783
1853
|
|
|
1784
|
-
if (status !== null && certStatus === null) certStatus = status;
|
|
1854
|
+
if (status !== null && certStatus === null) { certStatus = status.code; certTime = status.at; }
|
|
1785
1855
|
else if (status === null) reasonsMask |= gate.interim;
|
|
1786
1856
|
}
|
|
1787
1857
|
|
|
@@ -1793,14 +1863,15 @@ function crlChecker(crls, opts) {
|
|
|
1793
1863
|
sawUnmergedDelta = true;
|
|
1794
1864
|
var dStatus = scanCrl(dRec.crl);
|
|
1795
1865
|
if (dStatus === null) continue;
|
|
1796
|
-
if (dStatus === 8) { releasedByUnmergedDelta = true; continue; }
|
|
1797
|
-
if (certStatus === null) certStatus = dStatus;
|
|
1866
|
+
if (dStatus.code === 8) { releasedByUnmergedDelta = true; continue; }
|
|
1867
|
+
if (certStatus === null) { certStatus = dStatus.code; certTime = dStatus.at; }
|
|
1798
1868
|
}
|
|
1799
1869
|
|
|
1800
1870
|
if (releasedByUnmergedDelta) return guard.verdict.of({ status: "unknown", reason: "a delta CRL released this serial from hold; without merging its base CRL the revocation status is undetermined" });
|
|
1801
1871
|
if (certStatus !== null) {
|
|
1802
1872
|
var reasonName = constants.NAMES.CRL_REASON[String(certStatus)] || "unspecified";
|
|
1803
|
-
return guard.verdict.of({ status: "revoked", reasonCode: certStatus, reason: "serial listed in a CRL (" + reasonName + ")" }
|
|
1873
|
+
return guard.verdict.of({ status: "revoked", reasonCode: certStatus, reason: "serial listed in a CRL (" + reasonName + ")" },
|
|
1874
|
+
certTime === null ? null : { revocationTime: certTime });
|
|
1804
1875
|
}
|
|
1805
1876
|
if (sawUnmergedDelta) return guard.verdict.of({ status: "unknown", reason: "a delta CRL cannot be combined with any complete CRL held here, so the revocation picture is incomplete" });
|
|
1806
1877
|
if ((reasonsMask & ALL_REASONS) === ALL_REASONS) return guard.verdict.of({ status: "good" });
|
|
@@ -1816,6 +1887,7 @@ function crlChecker(crls, opts) {
|
|
|
1816
1887
|
}
|
|
1817
1888
|
|
|
1818
1889
|
var OID_REASON_CODE = oid.byName("reasonCode");
|
|
1890
|
+
var OID_OCSP_NONCE = oid.byName("ocspNonce");
|
|
1819
1891
|
function crlEntryReason(entry) {
|
|
1820
1892
|
var exts = entry.crlEntryExtensions || [];
|
|
1821
1893
|
for (var i = 0; i < exts.length; i++) {
|
|
@@ -1939,17 +2011,16 @@ function ocspChecker(responses) {
|
|
|
1939
2011
|
|
|
1940
2012
|
/**
|
|
1941
2013
|
* @primitive pki.path.verifyOcspResponse
|
|
1942
|
-
* @signature pki.path.verifyOcspResponse(response, cert, issuerCert, time, opts?) -> Promise<{ valid, status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, revocationTime?, reason }>
|
|
2014
|
+
* @signature pki.path.verifyOcspResponse(response, cert, issuerCert, time, opts?) -> Promise<{ valid, status, responderAuthorized, signatureValid, matched, nonceMatched, thisUpdate, nextUpdate, revocationReason?, revocationTime?, reason }>
|
|
1943
2015
|
* @since 0.2.22
|
|
1944
2016
|
* @status stable
|
|
1945
|
-
* @spec RFC 6960
|
|
2017
|
+
* @spec RFC 6960, RFC 9654
|
|
1946
2018
|
* @related pki.ocsp.verify, pki.path.ocspChecker
|
|
1947
2019
|
*
|
|
1948
2020
|
* Verify a single OCSP response for one certificate
|
|
1949
2021
|
* against its already-parsed issuer certificate at `time`. This is the lower-level
|
|
1950
2022
|
* primitive `pki.ocsp.verify` composes after parsing its inputs (most callers want
|
|
1951
|
-
* that ergonomic entry, which also handles DER/PEM decoding
|
|
1952
|
-
* matching). It runs exactly the gates the path validator's `ocspChecker`
|
|
2023
|
+
* that ergonomic entry, which also handles DER/PEM decoding). It runs exactly the gates the path validator's `ocspChecker`
|
|
1953
2024
|
* does: it locates the SingleResponse whose CertID binds this cert's serial to
|
|
1954
2025
|
* its issuer (recomputing `issuerNameHash`/`issuerKeyHash` under the CertID's
|
|
1955
2026
|
* own hashAlgorithm), confirms the responder is authorized (the issuing CA
|
|
@@ -1967,6 +2038,13 @@ function ocspChecker(responses) {
|
|
|
1967
2038
|
* `time` either way. `time` must be a valid `Date`. A malformed response's parse
|
|
1968
2039
|
* fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
|
|
1969
2040
|
*
|
|
2041
|
+
* `opts.requestNonce` is the nonce bytes the request carried. Given one, the response must echo it
|
|
2042
|
+
* (RFC 9654 sec. 2.1, RFC 6960 sec. 4.4.1), compared in constant time, and `nonceMatched` reports
|
|
2043
|
+
* the outcome: `true`, `false`, or `null` when no nonce was sent. An unechoed nonce takes a `good`
|
|
2044
|
+
* to `unknown`, because a response that does not carry it answers an earlier question; a `revoked`
|
|
2045
|
+
* keeps its status and carries `nonceMatched: false`, a stale revocation being no safer to accept.
|
|
2046
|
+
* This is where the rule is applied, so every caller reaching this gate gets it.
|
|
2047
|
+
*
|
|
1970
2048
|
* The response is its DER bytes, a PEM string, or an unmodified
|
|
1971
2049
|
* `pki.schema.ocsp.parseResponse` result. A REBUILT parsed response is refused. A
|
|
1972
2050
|
* signature check has three parts (the signature, the algorithm that verifies it,
|
|
@@ -1995,14 +2073,23 @@ function ocspChecker(responses) {
|
|
|
1995
2073
|
* var v = await pki.path.verifyOcspResponse(der, cert, issuerCert, new Date());
|
|
1996
2074
|
* v.status; // "good" | "revoked" | "unknown"
|
|
1997
2075
|
*/
|
|
2076
|
+
var _VERIFY_OCSP_OPTS = intrinsic.assign(intrinsic.create(null), { historicalMode: 1, requestNonce: 1 });
|
|
2077
|
+
|
|
1998
2078
|
async function verifyOcspResponse(response, cert, issuerCert, time, opts) {
|
|
1999
|
-
var parsedResponse, subject, issuer;
|
|
2079
|
+
var parsedResponse, subject, issuer, checkedOpts;
|
|
2000
2080
|
try {
|
|
2081
|
+
/** @internal The options go through the same door every other verb's do. Without it a
|
|
2082
|
+
* misspelled `requestNonce` leaves the nonce unasked and the response is accepted against
|
|
2083
|
+
* bytes nobody compared, and an accessor supplies one value to the check and another to the
|
|
2084
|
+
* comparison. This verb is the acceptance gate, so both end at a response reported good. */
|
|
2085
|
+
checkedOpts = guard.identifier.optionsObject(opts, E, "path/bad-input", "verifyOcspResponse: opts");
|
|
2086
|
+
guard.identifier.assertKnownKeys(checkedOpts, _VERIFY_OCSP_OPTS, E, "path/bad-input",
|
|
2087
|
+
"pki.path.verifyOcspResponse has an unknown option (the request nonce to bind against is `requestNonce`). The unknown option was: ");
|
|
2001
2088
|
parsedResponse = _ocspFromBytes(response);
|
|
2002
2089
|
subject = coerceCert(cert, "the certificate");
|
|
2003
2090
|
issuer = coerceCert(issuerCert, "the issuer certificate");
|
|
2004
2091
|
} catch (e) { return Promise.reject(e); }
|
|
2005
|
-
var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time,
|
|
2092
|
+
var v = await _verifyOcspParsed(parsedResponse, subject, issuer, time, checkedOpts);
|
|
2006
2093
|
var out = guard.verdict.of(v);
|
|
2007
2094
|
return guard.verdict.of(out, { valid: out.status === "good" });
|
|
2008
2095
|
}
|
|
@@ -2023,7 +2110,21 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2023
2110
|
if (!guard.time.isDate(time) || isNaN(guard.time.instantOf(time))) {
|
|
2024
2111
|
return Promise.reject(E("path/bad-input", "verifyOcspResponse: time must be a valid Date (the currency + responder-validity check date)"));
|
|
2025
2112
|
}
|
|
2026
|
-
|
|
2113
|
+
/** @internal Snapshotted in the synchronous prologue: the bytes compared are the bytes the caller
|
|
2114
|
+
* passed, not what its BufferSource holds once the verify has awaited. A value that is not bytes
|
|
2115
|
+
* at all is not a refusal, because a caller that asked for a nonce and supplied something that
|
|
2116
|
+
* cannot be one has still asked: `wantNonce` stays set with nothing to compare, so the response
|
|
2117
|
+
* cannot echo it and the verdict fails closed rather than passing the check by skipping it. */
|
|
2118
|
+
var wantNonce = opts.requestNonce != null;
|
|
2119
|
+
var nonce = (wantNonce && guard.bytes.isByteSource(opts.requestNonce))
|
|
2120
|
+
? guard.bytes.snapshotSource(opts.requestNonce, PathError, "path/bad-input", "opts.requestNonce")
|
|
2121
|
+
: null;
|
|
2122
|
+
/** @internal A verdict decided before the response is read is still a verdict, and the field
|
|
2123
|
+
* that says what became of the nonce is on every one of them: absent is not the same answer as
|
|
2124
|
+
* "no nonce was sent", which is what `null` says. */
|
|
2125
|
+
function unbound(reason) {
|
|
2126
|
+
return _withNonceRule(guard.verdict.of({ status: "unknown", responderAuthorized: false, signatureValid: false, matched: false, thisUpdate: null, nextUpdate: null, reason: reason }), parsedResponse, wantNonce, nonce);
|
|
2127
|
+
}
|
|
2027
2128
|
var boundName;
|
|
2028
2129
|
try { boundName = dnEqual(cert.issuer.rdns, issuerCert.subject.rdns); }
|
|
2029
2130
|
catch (e) { return Promise.reject(e); }
|
|
@@ -2040,13 +2141,39 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2040
2141
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
|
|
2041
2142
|
catch (_e) { return unbound("the issuer public key could not be read to recompute the OCSP CertID"); }
|
|
2042
2143
|
return ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits, issuerNameCandidates, time, opts.historicalMode === true).then(function (v) {
|
|
2043
|
-
if (v.revoked) return guard.verdict.of({ status: "revoked", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, revocationReason: v.revoked.revocationReason, revocationTime: v.revoked.revocationTime, reason: v.revoked.reason });
|
|
2044
|
-
if (v.sawGood) return guard.verdict.of({ status: "good", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: "good" });
|
|
2045
|
-
return guard.verdict.of({ status: "unknown", responderAuthorized: v.responderAuthorized === true, signatureValid: v.signatureValid === true, matched: v.matched === true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: v.reason });
|
|
2144
|
+
if (v.revoked) return _withNonceRule(guard.verdict.of({ status: "revoked", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, revocationReason: v.revoked.revocationReason, revocationTime: v.revoked.revocationTime, reason: v.revoked.reason }), parsedResponse, wantNonce, nonce);
|
|
2145
|
+
if (v.sawGood) return _withNonceRule(guard.verdict.of({ status: "good", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: "good" }), parsedResponse, wantNonce, nonce);
|
|
2146
|
+
return _withNonceRule(guard.verdict.of({ status: "unknown", responderAuthorized: v.responderAuthorized === true, signatureValid: v.signatureValid === true, matched: v.matched === true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: v.reason }), parsedResponse, wantNonce, nonce);
|
|
2046
2147
|
});
|
|
2047
2148
|
});
|
|
2048
2149
|
}
|
|
2049
2150
|
|
|
2151
|
+
/** @internal RFC 9654 sec. 2.1 with RFC 6960 sec. 4.4.1: a client that sent a nonce is asking this
|
|
2152
|
+
* responder this question now, and a response that does not echo it answers some earlier question.
|
|
2153
|
+
* The rule lives here because this is the acceptance gate every OCSP consumer reaches, and a copy
|
|
2154
|
+
* above it would leave a consumer that does not go through that copy with no check at all.
|
|
2155
|
+
*
|
|
2156
|
+
* An unechoed nonce takes a `good` to undetermined and leaves a `revoked` standing: a response
|
|
2157
|
+
* saying the certificate is revoked is not made safe by being stale, so the verdict keeps its
|
|
2158
|
+
* status and reports `nonceMatched: false` beside it. The comparison is constant-time. */
|
|
2159
|
+
function _withNonceRule(verdict, parsedResponse, wantNonce, requestNonce) {
|
|
2160
|
+
if (!wantNonce) return guard.verdict.of(verdict, { nonceMatched: null });
|
|
2161
|
+
var respNonce = _responseNonce(parsedResponse);
|
|
2162
|
+
var matched = requestNonce !== null && respNonce != null &&
|
|
2163
|
+
guard.crypto.constantTimeEqual(respNonce, requestNonce);
|
|
2164
|
+
var out = guard.verdict.of(verdict, { nonceMatched: matched });
|
|
2165
|
+
if (matched || out.status !== "good") return out;
|
|
2166
|
+
return guard.verdict.of(out, { status: "unknown",
|
|
2167
|
+
reason: "the OCSP response nonce does not echo the request nonce (RFC 9654 sec. 2.1)" });
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function _responseNonce(parsedResponse) {
|
|
2171
|
+
var br = parsedResponse && parsedResponse.basicResponse;
|
|
2172
|
+
var exts = (br && br.responseExtensions) || [];
|
|
2173
|
+
for (var i = 0; i < exts.length; i++) if (exts[i].oid === OID_OCSP_NONCE) return exts[i].nonce || null;
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2050
2177
|
|
|
2051
2178
|
function softDecode(cert, extOid) {
|
|
2052
2179
|
try { return decodeExt(cert, extOid); }
|
|
@@ -2857,7 +2984,12 @@ async function build(leaf, opts) {
|
|
|
2857
2984
|
var aiaTransport = checkedTransport || httpTransport.https({ E: E, errPrefix: "path" });
|
|
2858
2985
|
aiaCtx = {
|
|
2859
2986
|
transport: aiaTransport,
|
|
2860
|
-
|
|
2987
|
+
/** @internal Read off the slot, not the chain: `Function.prototype` carries whatever has
|
|
2988
|
+
* been written to it for every function in the process, so a transport that declares
|
|
2989
|
+
* nothing would otherwise be read as promising it filters the address it resolved, and a
|
|
2990
|
+
* hostname taken from a certificate would be opened on that promise.
|
|
2991
|
+
* @guard-via guard.identifier.declaresOwn */
|
|
2992
|
+
transportGuardsAddresses: guard.identifier.declaresOwn(aiaTransport, "blocksPrivateAddresses"),
|
|
2861
2993
|
tls: opts.tls || {},
|
|
2862
2994
|
timeout: opts.aiaTimeout,
|
|
2863
2995
|
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 }),
|
|
@@ -3027,11 +3159,17 @@ async function build(leaf, opts) {
|
|
|
3027
3159
|
throw E("path/no-path", "build: no certification path from the leaf to any configured trust anchor could be assembled");
|
|
3028
3160
|
}
|
|
3029
3161
|
|
|
3162
|
+
/** @internal The fetching checker composes the two gates below, so it loads after this module
|
|
3163
|
+
* rather than beside it. The require sits in the call so the cycle resolves at first use, which is
|
|
3164
|
+
* the documented reason an inline require is permitted. */
|
|
3165
|
+
function fetchingChecker(opts) { return require("./revocation-fetch").fetchingChecker(opts); }
|
|
3166
|
+
|
|
3030
3167
|
module.exports = {
|
|
3031
3168
|
validate: validate,
|
|
3032
3169
|
build: build,
|
|
3033
3170
|
anchorFromCert: anchorFromCert,
|
|
3034
3171
|
crlChecker: crlChecker,
|
|
3172
|
+
fetchingChecker: fetchingChecker,
|
|
3035
3173
|
ocspChecker: ocspChecker,
|
|
3036
3174
|
verifyOcspResponse: verifyOcspResponse,
|
|
3037
3175
|
PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
|