@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +143 -76
  2. package/README.md +4 -2
  3. package/index.js +11 -0
  4. package/lib/acme.js +15 -78
  5. package/lib/asn1-der.js +58 -6
  6. package/lib/cbor-det.js +11 -1
  7. package/lib/cmc-build.js +1 -1
  8. package/lib/cmc-verify.js +3 -3
  9. package/lib/cmp-session.js +1 -1
  10. package/lib/cms-sign.js +3 -3
  11. package/lib/cms-verify.js +5 -5
  12. package/lib/constants.js +11 -0
  13. package/lib/est.js +1 -1
  14. package/lib/framework-error.js +8 -0
  15. package/lib/guard-async.js +53 -2
  16. package/lib/guard-encoding.js +105 -1
  17. package/lib/guard-identifier.js +74 -1
  18. package/lib/guard-intrinsic.js +2 -0
  19. package/lib/guard-json.js +1 -6
  20. package/lib/guard-limits.js +40 -1
  21. package/lib/guard-list.js +25 -0
  22. package/lib/guard-name.js +134 -22
  23. package/lib/guard-text.js +33 -1
  24. package/lib/http-transport.js +39 -7
  25. package/lib/identity-match.js +561 -0
  26. package/lib/ip-utils.js +52 -1
  27. package/lib/key.js +250 -5
  28. package/lib/ocsp.js +125 -22
  29. package/lib/oid.js +1 -1
  30. package/lib/path-validate.js +199 -61
  31. package/lib/pkcs11-uri.js +646 -0
  32. package/lib/pkcs12-build.js +6 -1
  33. package/lib/revocation-fetch.js +772 -0
  34. package/lib/schema-all.js +124 -0
  35. package/lib/schema-attrcert.js +4 -2
  36. package/lib/schema-crmf.js +4 -2
  37. package/lib/schema-pkcs1.js +335 -0
  38. package/lib/schema-pkcs8.js +15 -0
  39. package/lib/schema-pkix.js +322 -40
  40. package/lib/schema-sec1.js +244 -0
  41. package/lib/schema-tsp.js +4 -2
  42. package/lib/schema-x509.js +4 -2
  43. package/lib/trust.js +2 -2
  44. package/lib/tsp-sign.js +1 -1
  45. package/lib/webauthn.js +4 -4
  46. package/lib/webcrypto.js +17 -5
  47. package/package.json +1 -1
  48. package/sbom.cdx.json +6 -6
@@ -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 _SCHEME_TABLE = _charTable("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-");
500
- function _uriAuthorityRange(uri) {
501
- var n = uri.length;
502
- if (n === 0) return null;
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 r = _uriAuthorityRange(uri);
537
- if (r === null) return null;
538
- var authority = uri.slice(r[0], r[1]);
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. A
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
- opts = guard.identifier.optionsObject(opts, E, "path/bad-input", "validate: opts");
1143
- guard.identifier.assertKnownKeys(opts, _VALIDATE_OPTS, E, "path/bad-input",
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 validate(path, singleOpts(tuples[ai]));
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 : validate(path, singleOpts(tuples[0]));
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
- if (revocationChecker) {
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`) and a `reason` naming it.
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 and request-nonce
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, opts);
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
- function unbound(reason) { return guard.verdict.of({ status: "unknown", responderAuthorized: false, signatureValid: false, matched: false, thisUpdate: null, nextUpdate: null, reason: reason }); }
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
- transportGuardsAddresses: aiaTransport.blocksPrivateAddresses === true,
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,