@blamejs/pki 0.1.31 → 0.2.0
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 +33 -0
- package/README.md +2 -1
- package/index.js +7 -0
- package/lib/asn1-der.js +31 -22
- package/lib/constants.js +29 -0
- package/lib/ct.js +13 -12
- package/lib/est.js +10 -10
- package/lib/framework-error.js +11 -0
- package/lib/guard-all.js +15 -0
- package/lib/guard-crypto.js +31 -1
- package/lib/guard-encoding.js +90 -0
- package/lib/guard-identifier.js +56 -0
- package/lib/guard-json.js +149 -0
- package/lib/guard-limits.js +58 -8
- package/lib/guard-name.js +61 -3
- package/lib/guard-range.js +40 -7
- package/lib/guard-text.js +7 -0
- package/lib/jose.js +17 -128
- package/lib/merkle.js +7 -16
- package/lib/oid.js +22 -9
- package/lib/path-validate.js +461 -56
- package/lib/schema-attrcert.js +5 -0
- package/lib/schema-cms.js +22 -1
- package/lib/schema-crmf.js +3 -5
- package/lib/schema-ocsp.js +5 -1
- package/lib/schema-pkix.js +92 -12
- package/lib/trust.js +707 -0
- package/lib/webcrypto.js +18 -7
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
|
|
|
37
37
|
var schema = require("./schema-engine");
|
|
38
38
|
var x509 = require("./schema-x509");
|
|
39
39
|
var crl = require("./schema-crl");
|
|
40
|
+
var ocsp = require("./schema-ocsp");
|
|
40
41
|
var guard = require("./guard-all");
|
|
41
42
|
var constants = require("./constants");
|
|
42
43
|
|
|
@@ -69,6 +70,7 @@ var OID = {
|
|
|
69
70
|
emailAddress: oid.byName("emailAddress"),
|
|
70
71
|
extKeyUsage: oid.byName("extKeyUsage"),
|
|
71
72
|
anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
|
|
73
|
+
cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
// The set of extension OIDs the validator PROCESSES -- an unrecognized critical
|
|
@@ -79,9 +81,15 @@ var OID = {
|
|
|
79
81
|
// is the caller's opt-in via opts.requiredEku (RFC 5280 6.1 defines no EKU
|
|
80
82
|
// processing step -- the required purpose is application context).
|
|
81
83
|
var PROCESSED_EXTENSIONS = {};
|
|
84
|
+
// cRLDistributionPoints is processed: the CRL checker consults it for the
|
|
85
|
+
// sec. 6.3.3 shard correspondence, and a critical instance (sec. 4.2.1.13 is a
|
|
86
|
+
// SHOULD-non-critical) is structurally validated by
|
|
87
|
+
// validateCriticalExtensionStructure via the registered decoder. freshestCRL
|
|
88
|
+
// stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
|
|
89
|
+
// consult it (no delta merge), so a critical instance fails unrecognized.
|
|
82
90
|
[OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
|
|
83
91
|
OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
|
|
84
|
-
OID.extKeyUsage].
|
|
92
|
+
OID.extKeyUsage, OID.cRLDistributionPoints].
|
|
85
93
|
forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
|
|
86
94
|
|
|
87
95
|
// ---- signature verify bridge (NEW 6) ---------------------------------------
|
|
@@ -317,7 +325,7 @@ function builtinVerify(state, cert) {
|
|
|
317
325
|
} catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
|
|
318
326
|
// The signature is an octet-aligned BIT STRING (no unused bits) for every
|
|
319
327
|
// supported algorithm; a non-zero unused-bit count is malformed.
|
|
320
|
-
if (cert.signatureValue
|
|
328
|
+
if (!guard.crypto.isOctetAligned(cert.signatureValue)) return Promise.resolve({ ok: false, code: "path/bad-signature" });
|
|
321
329
|
var key;
|
|
322
330
|
return subtle.importKey("spki", state.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
|
|
323
331
|
key = k;
|
|
@@ -336,42 +344,17 @@ function builtinVerify(state, cert) {
|
|
|
336
344
|
|
|
337
345
|
// ---- 7.1 name comparison ---------------------------------------------------
|
|
338
346
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// cannot make two different names compare equal.
|
|
347
|
-
if (typeof v !== "string") return v;
|
|
348
|
-
guard.name.assertNoControlBytes(v, E, "path/name-chaining", "distinguished name");
|
|
349
|
-
return v.trim().replace(/\s+/g, " ").toLowerCase();
|
|
347
|
+
// RFC 5280 sec. 7.1 canonical DN / RDN comparison, via the shared name guard: the
|
|
348
|
+
// canonical form (case-fold + internal-whitespace collapse) and the embedded
|
|
349
|
+
// control-byte reject (CVE-2009-2408 -> path/name-chaining) live once in
|
|
350
|
+
// guard-name, so no path-validation caller can reintroduce a raw-byte DN
|
|
351
|
+
// comparison that treats two RFC 5280-equal names as different.
|
|
352
|
+
function dnEqual(rdnsA, rdnsB) {
|
|
353
|
+
return guard.name.dnEqual(rdnsA, rdnsB, E, "path/name-chaining", "distinguished name");
|
|
350
354
|
}
|
|
351
355
|
|
|
352
356
|
function rdnEqual(a, b) {
|
|
353
|
-
|
|
354
|
-
// An RDN is an unordered SET of type/value pairs; compare as multisets.
|
|
355
|
-
var used = [];
|
|
356
|
-
for (var i = 0; i < a.length; i++) {
|
|
357
|
-
var found = false;
|
|
358
|
-
for (var j = 0; j < b.length; j++) {
|
|
359
|
-
if (used[j]) continue;
|
|
360
|
-
if (a[i].type === b[j].type && normalizeAttrValue(a[i].value) === normalizeAttrValue(b[j].value)) {
|
|
361
|
-
used[j] = true; found = true; break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (!found) return false;
|
|
365
|
-
}
|
|
366
|
-
return true;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function dnEqual(rdnsA, rdnsB) {
|
|
370
|
-
if (rdnsA.length !== rdnsB.length) return false;
|
|
371
|
-
for (var i = 0; i < rdnsA.length; i++) {
|
|
372
|
-
if (!rdnEqual(rdnsA[i], rdnsB[i])) return false;
|
|
373
|
-
}
|
|
374
|
-
return true;
|
|
357
|
+
return guard.name.rdnEqual(a, b, E, "path/name-chaining", "distinguished name");
|
|
375
358
|
}
|
|
376
359
|
|
|
377
360
|
// ---- extension access ------------------------------------------------------
|
|
@@ -1074,8 +1057,7 @@ async function validate(path, opts) {
|
|
|
1074
1057
|
// Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
|
|
1075
1058
|
// (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
|
|
1076
1059
|
// crypto amplification from an oversized path). Entry-point tier: throw.
|
|
1077
|
-
var maxCerts = opts.maxPathCerts
|
|
1078
|
-
if (typeof maxCerts !== "number" || !isFinite(maxCerts) || maxCerts < 1) throw E("path/bad-input", "validate: opts.maxPathCerts must be a positive number");
|
|
1060
|
+
var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
|
|
1079
1061
|
if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
|
|
1080
1062
|
var certs = path.map(function (c) { return (c && c.tbsBytes) ? c : x509.parse(c); });
|
|
1081
1063
|
var n = certs.length;
|
|
@@ -1088,9 +1070,9 @@ async function validate(path, opts) {
|
|
|
1088
1070
|
}
|
|
1089
1071
|
// Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
|
|
1090
1072
|
// throws here rather than silently disabling the behavior it configures.
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1073
|
+
// Validate-if-present (the default is applied where the policy state is built);
|
|
1074
|
+
// the shared integer cap rejects a fractional / negative / non-numeric budget.
|
|
1075
|
+
guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
|
|
1094
1076
|
// user-initial-policy-set is a non-empty SET of policy OID strings (6.1.1(c)).
|
|
1095
1077
|
// Membership is tested with indexOf, so a raw string here would be consulted
|
|
1096
1078
|
// as a SUBSTRING match -- validated to an array of strings instead.
|
|
@@ -1098,6 +1080,11 @@ async function validate(path, opts) {
|
|
|
1098
1080
|
var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
|
|
1099
1081
|
opts.userInitialPolicySet.every(function (p) { return typeof p === "string" && p.length > 0; });
|
|
1100
1082
|
if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
|
|
1083
|
+
// Each entry is compared (indexOf) against canonical decoder output, so a
|
|
1084
|
+
// non-canonical key would silently never match -- fail the typo closed here.
|
|
1085
|
+
opts.userInitialPolicySet.forEach(function (p) {
|
|
1086
|
+
guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.userInitialPolicySet entry " + JSON.stringify(p));
|
|
1087
|
+
});
|
|
1101
1088
|
}
|
|
1102
1089
|
var seeds = {
|
|
1103
1090
|
permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
|
|
@@ -1113,12 +1100,33 @@ async function validate(path, opts) {
|
|
|
1113
1100
|
}
|
|
1114
1101
|
requiredEku = opts.requiredEku.map(function (p) {
|
|
1115
1102
|
if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
|
|
1116
|
-
|
|
1103
|
+
// A dotted-form attempt (leads with a digit) must be a canonical OID -- a
|
|
1104
|
+
// loose regex accepted a leading-zero / out-of-bounds key that would never
|
|
1105
|
+
// match the canonical EKU the target advertises; anything else is a name.
|
|
1106
|
+
if (/^[0-9]/.test(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
|
|
1117
1107
|
var dotted = oid.byName(p);
|
|
1118
1108
|
if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
|
|
1119
1109
|
return dotted;
|
|
1120
1110
|
});
|
|
1121
1111
|
}
|
|
1112
|
+
// opts.checkPurpose -- the single key purpose the ANCHOR's NSS trust metadata
|
|
1113
|
+
// (distrustAfter / purposes) is consulted for. Independent of requiredEku
|
|
1114
|
+
// (which gates the leaf's own EKU extension): this selects the per-purpose
|
|
1115
|
+
// key in the trust-anchor constraint contract. A purpose OID name (or a
|
|
1116
|
+
// canonical dotted OID normalized to its name); a bad value throws here.
|
|
1117
|
+
var checkPurpose = null;
|
|
1118
|
+
if (opts.checkPurpose !== undefined) {
|
|
1119
|
+
if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
|
|
1120
|
+
throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
|
|
1121
|
+
}
|
|
1122
|
+
if (/^[0-9]/.test(opts.checkPurpose)) {
|
|
1123
|
+
var cpDotted = guard.identifier.assertCanonicalOid(opts.checkPurpose, E, "path/bad-input", "validate: opts.checkPurpose");
|
|
1124
|
+
checkPurpose = oid.name(cpDotted) || cpDotted; // normalize a dotted purpose OID to its name for the anchor map
|
|
1125
|
+
} else {
|
|
1126
|
+
if (typeof oid.byName(opts.checkPurpose) !== "string") throw E("path/bad-input", "validate: opts.checkPurpose " + JSON.stringify(opts.checkPurpose) + " is not a registered OID name");
|
|
1127
|
+
checkPurpose = opts.checkPurpose;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1122
1130
|
|
|
1123
1131
|
var state = initialize(certs, opts, seeds);
|
|
1124
1132
|
state._n = n;
|
|
@@ -1257,6 +1265,22 @@ async function validate(path, opts) {
|
|
|
1257
1265
|
// default). An ABSENT extension leaves the key unrestricted, so it
|
|
1258
1266
|
// satisfies any required purpose.
|
|
1259
1267
|
if (requiredEku && ekuPurposeFails(cert, requiredEku, checks)) failed = true;
|
|
1268
|
+
// Trust-anchor constraint contract (NSS / CCADB metadata; gated so a bare
|
|
1269
|
+
// anchor or an absent checkPurpose is byte-identical to today). The anchor's
|
|
1270
|
+
// per-purpose distrust-after date and delegator purposes apply to the
|
|
1271
|
+
// end-entity leaf it ultimately certifies.
|
|
1272
|
+
var ta = opts.trustAnchor;
|
|
1273
|
+
if (checkPurpose && ta.distrustAfter && ta.distrustAfter[checkPurpose] instanceof Date &&
|
|
1274
|
+
cert.validity.notBefore > ta.distrustAfter[checkPurpose]) {
|
|
1275
|
+
// STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
|
|
1276
|
+
// (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
|
|
1277
|
+
// <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
|
|
1278
|
+
// convention keeps the whole boundary day trusted).
|
|
1279
|
+
checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
|
|
1280
|
+
}
|
|
1281
|
+
if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
|
|
1282
|
+
checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
|
|
1283
|
+
}
|
|
1260
1284
|
updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
|
|
1261
1285
|
}
|
|
1262
1286
|
|
|
@@ -1354,11 +1378,24 @@ var IDP_SCHEMA = schema.seq([
|
|
|
1354
1378
|
function decodeIdp(ext) {
|
|
1355
1379
|
// Surface the scope flags the checker gates on. ANY structural or value fault
|
|
1356
1380
|
// -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
|
|
1357
|
-
// an encoded-FALSE default
|
|
1358
|
-
// unusable, never assumed unrestricted.
|
|
1359
|
-
var out = { hasDistributionPoint: false, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
|
|
1381
|
+
// an encoded-FALSE default, a malformed DistributionPointName -- leaves the
|
|
1382
|
+
// CRL's scope unknown: the CRL is unusable, never assumed unrestricted.
|
|
1383
|
+
var out = { hasDistributionPoint: false, distributionPoint: null, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
|
|
1360
1384
|
var m;
|
|
1361
|
-
try {
|
|
1385
|
+
try {
|
|
1386
|
+
m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS);
|
|
1387
|
+
if (m.fields.distributionPoint.present) {
|
|
1388
|
+
// distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE
|
|
1389
|
+
// (a context tag on a CHOICE-typed field is always EXPLICIT). The decoded
|
|
1390
|
+
// name feeds the sec. 6.3.3(b)(2)(i) correspondence against the
|
|
1391
|
+
// certificate's own DistributionPoints in the checker below.
|
|
1392
|
+
var dpnWrap = m.fields.distributionPoint.node;
|
|
1393
|
+
if (!dpnWrap.children || dpnWrap.children.length !== 1) {
|
|
1394
|
+
throw E("path/bad-idp", "IssuingDistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
|
|
1395
|
+
}
|
|
1396
|
+
out.distributionPoint = pkix.distributionPointName(NS, dpnWrap.children[0], "path/bad-idp");
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1362
1399
|
catch (_e) { out.malformed = true; return out; }
|
|
1363
1400
|
function flag(f) {
|
|
1364
1401
|
if (!f.present) return false;
|
|
@@ -1377,6 +1414,58 @@ function decodeIdp(ext) {
|
|
|
1377
1414
|
return out;
|
|
1378
1415
|
}
|
|
1379
1416
|
|
|
1417
|
+
// RFC 5280 sec. 6.3.3(b)(2)(i): find the certificate DistributionPoint that
|
|
1418
|
+
// CORRESPONDS to a shard CRL's IDP distribution point name -- at least one
|
|
1419
|
+
// name in common between the two DistributionPointNames, compared by
|
|
1420
|
+
// BYTE-IDENTICAL DER encoding (Buffer.equals on the raw GeneralName / RDN
|
|
1421
|
+
// TLVs). sec. 5.2.5 pins the comparison key: "The identical encoding MUST be
|
|
1422
|
+
// used in the distributionPoint fields of the certificate and the CRL" -- so
|
|
1423
|
+
// a canonicalized or semantic comparison (which would equate two
|
|
1424
|
+
// differently-encoded names) is forbidden, and whole-set equality (which
|
|
1425
|
+
// would falsely reject a legitimate multi-URI DP sharing one name) is
|
|
1426
|
+
// over-strict. Mixed forms -- fullName on one side, nameRelativeToCRLIssuer
|
|
1427
|
+
// on the other -- never correspond here: resolving an RDN fragment against
|
|
1428
|
+
// the issuer DN is not attempted (fail closed). Per sec. 6.3.3(b)(1), a DP
|
|
1429
|
+
// naming a cRLIssuer participates only when that cRLIssuer is the certificate
|
|
1430
|
+
// issuer itself: the checker only consults CRLs issued BY the certificate
|
|
1431
|
+
// issuer and rejects indirect CRLs, so a DP delegated to another CRL issuer
|
|
1432
|
+
// is out of play. Returns the matched DP (its `reasons` feeds the coarse
|
|
1433
|
+
// reason-mask rule) or null.
|
|
1434
|
+
function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
|
|
1435
|
+
if (!idpDpn || !certDPs) return null;
|
|
1436
|
+
for (var i = 0; i < certDPs.length; i++) {
|
|
1437
|
+
var dp = certDPs[i];
|
|
1438
|
+
if (!dp.distributionPoint) continue;
|
|
1439
|
+
if (dp.cRLIssuer && !crlIssuerNamesIssuer(dp.cRLIssuer, issuerRdns)) continue;
|
|
1440
|
+
var cdpn = dp.distributionPoint;
|
|
1441
|
+
if (idpDpn.kind === "fullName" && cdpn.kind === "fullName") {
|
|
1442
|
+
for (var a = 0; a < idpDpn.names.length; a++) {
|
|
1443
|
+
for (var c = 0; c < cdpn.names.length; c++) {
|
|
1444
|
+
if (idpDpn.names[a].equals(cdpn.names[c])) return dp;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
} else if (idpDpn.kind === "rdn" && cdpn.kind === "rdn") {
|
|
1448
|
+
if (idpDpn.bytes.equals(cdpn.bytes)) return dp;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Does a DistributionPoint's cRLIssuer name the certificate issuer? True iff
|
|
1455
|
+
// one of its GeneralNames is a directoryName equal to the issuer DN under the
|
|
1456
|
+
// RFC 5280 sec. 7.1 comparison (the shared name guard, via this file's dnEqual
|
|
1457
|
+
// wrapper). Any fault -- a DN the comparison rejects for an embedded control
|
|
1458
|
+
// byte -- resolves false: the DP stays out of the correspondence (fail closed).
|
|
1459
|
+
function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
|
|
1460
|
+
for (var i = 0; i < cRLIssuer.names.length; i++) {
|
|
1461
|
+
var n = cRLIssuer.names[i];
|
|
1462
|
+
if (n.tagNumber !== 4 || !n.value || !n.value.rdns) continue;
|
|
1463
|
+
try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
|
|
1464
|
+
catch (_e) { return false; }
|
|
1465
|
+
}
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1380
1469
|
/**
|
|
1381
1470
|
* @primitive pki.path.crlChecker
|
|
1382
1471
|
* @signature pki.path.crlChecker(crls) -> RevocationChecker
|
|
@@ -1391,7 +1480,12 @@ function decodeIdp(ext) {
|
|
|
1391
1480
|
* verifies the CRL signature over its `tbsBytes`, honors the issuing
|
|
1392
1481
|
* distribution point scope and reason coverage, checks currency
|
|
1393
1482
|
* (`thisUpdate`/`nextUpdate`), and reports `{ status: "good"|"revoked"|
|
|
1394
|
-
* "unknown" }`.
|
|
1483
|
+
* "unknown" }`. A partitioned/sharded CRL (a critical IDP naming a
|
|
1484
|
+
* distribution point) establishes "good" when it corresponds to one of the
|
|
1485
|
+
* certificate's own cRLDistributionPoints -- at least one identically-encoded
|
|
1486
|
+
* name in common (RFC 5280 sec. 6.3.3) -- and neither side restricts reason
|
|
1487
|
+
* codes; a non-corresponding or reason-restricted shard is consulted for
|
|
1488
|
+
* revocation only. An out-of-scope, stale, unauthorized, or unverifiable CRL
|
|
1395
1489
|
* yields `unknown`, which the validator fails closed unless `softFail` is set.
|
|
1396
1490
|
*
|
|
1397
1491
|
* @example
|
|
@@ -1419,6 +1513,18 @@ function crlChecker(crls) {
|
|
|
1419
1513
|
} catch (e) {
|
|
1420
1514
|
certScopeFault = pathCode(e, "path/bad-extension-value");
|
|
1421
1515
|
}
|
|
1516
|
+
// The certificate's own cRLDistributionPoints, decoded once per check:
|
|
1517
|
+
// the sec. 6.3.3(b)(2)(i) correspondence gate below needs the DP names.
|
|
1518
|
+
// A decode fault leaves certDPs null -- every DP-scoped shard then stays
|
|
1519
|
+
// revocation-only (no correspondence can be shown; fail closed) while
|
|
1520
|
+
// the full-CRL path is unaffected (mirrors the basicConstraints
|
|
1521
|
+
// handling above); the check never crashes on a malformed extension.
|
|
1522
|
+
var certDPs = null;
|
|
1523
|
+
var certCdpExt = findExt(cert, OID.cRLDistributionPoints);
|
|
1524
|
+
if (certCdpExt) {
|
|
1525
|
+
try { certDPs = EXT_DECODERS[OID.cRLDistributionPoints](certCdpExt.value); }
|
|
1526
|
+
catch (_e) { certDPs = null; }
|
|
1527
|
+
}
|
|
1422
1528
|
// RFC 5280 sec. 6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
|
|
1423
1529
|
// certificate, the cRLSign bit must be VERIFIED set. An issuer that OMITS
|
|
1424
1530
|
// keyUsage is unconstrained (the same rule the sec. 6.1.4(n) keyCertSign gate
|
|
@@ -1513,7 +1619,34 @@ function crlChecker(crls) {
|
|
|
1513
1619
|
if (idp.onlyAttr) continue; // scoped to attribute certs, not this public-key cert
|
|
1514
1620
|
if (idp.onlyCa && certIsCa !== true) continue; // out of scope (or CA-ness undeterminable)
|
|
1515
1621
|
if (idp.onlyUser && certIsCa !== false) continue;
|
|
1516
|
-
if (idp.hasDistributionPoint
|
|
1622
|
+
if (idp.hasDistributionPoint) {
|
|
1623
|
+
// RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
|
|
1624
|
+
// certificate only when the IDP's distribution point shares at
|
|
1625
|
+
// least one IDENTICALLY-ENCODED name with one of the certificate's
|
|
1626
|
+
// own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
|
|
1627
|
+
// be used in the distributionPoint fields of the certificate and
|
|
1628
|
+
// the CRL"). The IDP must also be CRITICAL to be relied on for
|
|
1629
|
+
// scope: sec. 5.2.5 defines the IDP as "a critical CRL extension"
|
|
1630
|
+
// (descriptive phrasing, not an imperative MUST), and a partition
|
|
1631
|
+
// scope a non-supporting relying party would ignore is not a scope
|
|
1632
|
+
// to build "good" on -- a deliberate fail-closed decision. A
|
|
1633
|
+
// non-corresponding shard cannot establish "good" but is still
|
|
1634
|
+
// consulted for revocation below: serials are unique per issuer,
|
|
1635
|
+
// so a listed serial is a genuine revocation (fail closed toward
|
|
1636
|
+
// revoked).
|
|
1637
|
+
var matchedDp = idpExt.critical === true
|
|
1638
|
+
? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
|
|
1639
|
+
: null;
|
|
1640
|
+
if (!matchedDp) scopeRevocationOnly = true;
|
|
1641
|
+
// sec. 6.3.3(d)(3): a matched DP carrying `reasons` bounds the
|
|
1642
|
+
// interim reason mask below all-reasons -- under the coarse rule
|
|
1643
|
+
// ("good" only at the (d)(4) all-reasons case) that shard is
|
|
1644
|
+
// revocation-only.
|
|
1645
|
+
else if (matchedDp.reasons) scopeRevocationOnly = true;
|
|
1646
|
+
}
|
|
1647
|
+
// sec. 6.3.3(d)(1)/(d)(2): any onlySomeReasons restriction keeps the
|
|
1648
|
+
// interim reason mask below all-reasons -- revocation-only (coarse).
|
|
1649
|
+
if (idp.onlySomeReasons) scopeRevocationOnly = true;
|
|
1517
1650
|
}
|
|
1518
1651
|
if (theCrl.thisUpdate > time) continue; // not yet valid
|
|
1519
1652
|
// A CRL with no nextUpdate has no bounded validity -- its currency
|
|
@@ -1583,23 +1716,295 @@ function crlEntryReason(entry) {
|
|
|
1583
1716
|
return null;
|
|
1584
1717
|
}
|
|
1585
1718
|
|
|
1586
|
-
|
|
1719
|
+
// Verify a raw signature over tbsBytes with an SPKI public key -- the shared core
|
|
1720
|
+
// of every certificate / CRL / OCSP signature check. Resolve the algorithm
|
|
1721
|
+
// descriptor, enforce the key-OID <-> sig-OID binding (the algorithm-confusion
|
|
1722
|
+
// guard, RFC 9814 sec. 4), import the SPKI, bridge an ECDSA DER signature to
|
|
1723
|
+
// P1363, and verify. Any fault -- an unresolvable/forbidden algorithm, a
|
|
1724
|
+
// key/sig mismatch, an import or verify failure -- resolves false: a signature
|
|
1725
|
+
// check never throws out of this path, it fails closed. `rawSig` is the raw
|
|
1726
|
+
// signature octets (the caller has already unwrapped any BIT STRING and rejected
|
|
1727
|
+
// a non-octet-aligned one).
|
|
1728
|
+
function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
1587
1729
|
var d;
|
|
1588
1730
|
try {
|
|
1589
|
-
d = resolveDescriptor(
|
|
1590
|
-
assertKeyMatchesSigAlg(
|
|
1731
|
+
d = resolveDescriptor(sigAlg);
|
|
1732
|
+
assertKeyMatchesSigAlg(spkiBytes, sigAlg.oid, d);
|
|
1591
1733
|
} catch (_e) { return Promise.resolve(false); }
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
return subtle.importKey("spki", issuer.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
|
|
1595
|
-
key = k;
|
|
1596
|
-
var sig = theCrl.signatureValue.bytes;
|
|
1734
|
+
return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]).then(function (key) {
|
|
1735
|
+
var sig = rawSig;
|
|
1597
1736
|
if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
|
|
1598
|
-
return subtle.verify(d.verify, key, sig,
|
|
1737
|
+
return subtle.verify(d.verify, key, sig, tbsBytes);
|
|
1599
1738
|
}).then(function (ok) { return ok === true; }, function () { return false; });
|
|
1600
1739
|
}
|
|
1601
1740
|
|
|
1741
|
+
function verifyCrlSignature(theCrl, issuer) {
|
|
1742
|
+
if (!guard.crypto.isOctetAligned(theCrl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
|
|
1743
|
+
return _verifyWithSpki(theCrl.signatureAlgorithm, theCrl.signatureValue.bytes, issuer.workingPublicKey, theCrl.tbsBytes);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// ---- the OCSP revocation checker (RFC 6960) -------------------------------
|
|
1747
|
+
|
|
1748
|
+
// CertID identity-hash algorithms (RFC 6960 sec. 4.1.1). Kept SEPARATE from the
|
|
1749
|
+
// signature SIG_ALGS/HASH_BY_OID set (which omits SHA-1 -- a SHA-1 *signature* is
|
|
1750
|
+
// refused, SHAttered): a CertID hash is an identity binding of an already-known
|
|
1751
|
+
// issuer, not a signature, so collision resistance is irrelevant to the lookup
|
|
1752
|
+
// and RFC 6960's default SHA-1 CertID MUST interoperate. A hash OID outside this
|
|
1753
|
+
// set cannot be reproduced -> no CertID match (fail closed), never an assumption.
|
|
1754
|
+
var OCSP_CERTID_HASHES = {};
|
|
1755
|
+
OCSP_CERTID_HASHES[oid.byName("sha1")] = "SHA-1";
|
|
1756
|
+
OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
|
|
1757
|
+
OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
|
|
1758
|
+
OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
|
|
1759
|
+
var OID_OCSP_SIGNING = oid.byName("ocspSigning");
|
|
1760
|
+
var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
|
|
1761
|
+
|
|
1762
|
+
// The subjectPublicKey BIT STRING VALUE (excluding the unused-bits octet) of an
|
|
1763
|
+
// SPKI DER -- the exact bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash
|
|
1764
|
+
// over (RFC 6960 sec. 4.1.1). Throws on a malformed SPKI; the caller fails closed.
|
|
1765
|
+
function ocspKeyValue(spkiDer) {
|
|
1766
|
+
return asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// The delegate responder's importable SPKI. If its key omits algorithm parameters
|
|
1770
|
+
// and inherits them from the issuing CA (same key algorithm -- an EC public key
|
|
1771
|
+
// whose SPKI omits the namedCurve, RFC 5280 sec. 4.1.2.7), splice the issuer's
|
|
1772
|
+
// parameters in so importKey("spki", ...) has a complete key; otherwise a valid
|
|
1773
|
+
// response would verify as unknown. Mirrors updateWorkingKey's inheritance.
|
|
1774
|
+
function ocspResponderSpki(rc, issuer) {
|
|
1775
|
+
var keyAlg = rc.subjectPublicKeyInfo.algorithm;
|
|
1776
|
+
if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
|
|
1777
|
+
var issuerOid, issuerParams;
|
|
1778
|
+
try {
|
|
1779
|
+
var alg = asn1.decode(issuer.workingPublicKey).children[0];
|
|
1780
|
+
issuerOid = asn1.read.oid(alg.children[0]);
|
|
1781
|
+
issuerParams = alg.children[1] ? alg.children[1].bytes : null;
|
|
1782
|
+
} catch (_e) { return rc.subjectPublicKeyInfo.bytes; }
|
|
1783
|
+
if (issuerOid === keyAlg.oid && !isNullOrAbsentParams(issuerParams)) {
|
|
1784
|
+
return spliceSpkiParameters(rc.subjectPublicKeyInfo, keyAlg.oid, issuerParams);
|
|
1785
|
+
}
|
|
1786
|
+
return rc.subjectPublicKeyInfo.bytes;
|
|
1787
|
+
}
|
|
1788
|
+
function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
|
|
1789
|
+
|
|
1790
|
+
// The checker implements NO OCSP response-extension semantics (the nonce is the
|
|
1791
|
+
// live client's, RFC 6960 sec. 4.4.1; CRL References / Archive Cutoff / Service
|
|
1792
|
+
// Locator are not consulted), so a CRITICAL responseExtension / singleExtension
|
|
1793
|
+
// may change a response's meaning in a way this code cannot honor -- treat it as
|
|
1794
|
+
// unusable (RFC 6960 sec. 4.4 / the RFC 5280 critical-extension contract, the OCSP
|
|
1795
|
+
// analogue of the crlChecker unhandled-critical skip). Returns true if `extList`
|
|
1796
|
+
// carries any critical extension.
|
|
1797
|
+
function ocspHasCriticalExtension(extList) {
|
|
1798
|
+
if (!extList) return false;
|
|
1799
|
+
for (var i = 0; i < extList.length; i++) if (extList[i].critical) return true;
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// A SingleResponse's CertID names the target cert IFF its serial equals the
|
|
1804
|
+
// target's AND issuerNameHash + issuerKeyHash, recomputed under the CertID's OWN
|
|
1805
|
+
// hashAlgorithm, match the issuer (RFC 6960 sec. 4.1.1). A serial-only match would
|
|
1806
|
+
// let a "good" for issuer A serial N answer a query for issuer B serial N
|
|
1807
|
+
// (cross-CA substitution), so BOTH issuer hashes must match under the responder's
|
|
1808
|
+
// chosen algorithm -- never an assumed SHA-1.
|
|
1809
|
+
//
|
|
1810
|
+
// `issuerNameCandidates` is every byte encoding of the (name-chaining-validated)
|
|
1811
|
+
// issuer DN to try: the checked cert's issuer field (RFC 6960 sec. 4.1.1) plus,
|
|
1812
|
+
// when available, the issuer certificate's own subject encoding. These are RFC
|
|
1813
|
+
// 5280 sec. 7.1-equal (they chained) but MAY differ in DER -- case, whitespace, a
|
|
1814
|
+
// PrintableString-vs-UTF8String DirectoryString choice -- and a responder MAY have
|
|
1815
|
+
// hashed any of them; matching the CertID against any is still the SAME validated
|
|
1816
|
+
// issuer, because the issuerKeyHash + serial + authorized signature carry the
|
|
1817
|
+
// binding. The key (raw octets, no encoding variance) is matched once.
|
|
1818
|
+
async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
|
|
1819
|
+
if (certID.serialNumberHex !== cert.serialNumberHex) return false;
|
|
1820
|
+
var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
|
|
1821
|
+
if (!hashName) return false; // an unreproducible hash -> the match cannot be confirmed
|
|
1822
|
+
var keyHash = await ocspDigest(hashName, issuerKeyBits);
|
|
1823
|
+
if (!certID.issuerKeyHash.equals(keyHash)) return false;
|
|
1824
|
+
for (var i = 0; i < issuerNameCandidates.length; i++) {
|
|
1825
|
+
if (certID.issuerNameHash.equals(await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
|
|
1826
|
+
}
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Resolve the signer of a BasicOCSPResponse to an AUTHORIZED responder's SPKI DER,
|
|
1831
|
+
// or null when none is authorized (RFC 6960 sec. 4.2.2.2). Two models: the issuing
|
|
1832
|
+
// CA signing directly (responderID identifies the issuer), or a CA-delegated
|
|
1833
|
+
// responder -- a certificate in `certs` issued by the SAME CA, valid at `time`,
|
|
1834
|
+
// bearing id-kp-OCSPSigning in its extendedKeyUsage. anyExtendedKeyUsage and an
|
|
1835
|
+
// ABSENT EKU do NOT authorize an OCSP delegate (the opposite of the path-validation
|
|
1836
|
+
// EKU default), so an ordinary leaf the CA issued cannot forge revocation status.
|
|
1837
|
+
// Fails closed at every branch (a malformed delegate, a control-byte DN, an
|
|
1838
|
+
// unreadable EKU -> skip that candidate).
|
|
1839
|
+
async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
|
|
1840
|
+
var rid = basicResponse.responderID;
|
|
1841
|
+
var matchesIssuer = false;
|
|
1842
|
+
try {
|
|
1843
|
+
if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
|
|
1844
|
+
else if (rid.byKey) matchesIssuer = rid.byKey.equals(await ocspDigest("SHA-1", issuerKeyBits));
|
|
1845
|
+
} catch (_e) { matchesIssuer = false; }
|
|
1846
|
+
if (matchesIssuer) return issuer.workingPublicKey;
|
|
1847
|
+
|
|
1848
|
+
for (var i = 0; i < basicResponse.certs.length; i++) {
|
|
1849
|
+
var rc;
|
|
1850
|
+
try { rc = x509.parse(basicResponse.certs[i]); }
|
|
1851
|
+
catch (_e) { continue; }
|
|
1852
|
+
var identifies = false;
|
|
1853
|
+
try {
|
|
1854
|
+
if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
|
|
1855
|
+
else if (rid.byKey) identifies = rid.byKey.equals(await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
|
|
1856
|
+
} catch (_e) { identifies = false; }
|
|
1857
|
+
if (!identifies) continue;
|
|
1858
|
+
// The delegate MUST be issued directly by the CA that issued the target.
|
|
1859
|
+
var issuedByCa;
|
|
1860
|
+
try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
|
|
1861
|
+
catch (_e) { continue; }
|
|
1862
|
+
if (!issuedByCa) continue;
|
|
1863
|
+
if (!guard.crypto.isOctetAligned(rc.signatureValue)) continue;
|
|
1864
|
+
if (!(await _verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
|
|
1865
|
+
// The delegate certificate MUST itself be valid at the validation instant.
|
|
1866
|
+
if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
|
|
1867
|
+
// The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
|
|
1868
|
+
var eku;
|
|
1869
|
+
try { eku = decodeExt(rc, OID.extKeyUsage); }
|
|
1870
|
+
catch (_e) { continue; }
|
|
1871
|
+
if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
|
|
1872
|
+
// A delegate asserting keyUsage MUST permit digitalSignature -- signing an OCSP
|
|
1873
|
+
// response is a digitalSignature operation (RFC 5280 sec. 4.2.1.3); an absent
|
|
1874
|
+
// keyUsage is unrestricted, an unreadable one is not authoritative.
|
|
1875
|
+
var ku;
|
|
1876
|
+
try { ku = decodeExt(rc, OID.keyUsage); }
|
|
1877
|
+
catch (_e) { continue; }
|
|
1878
|
+
if (ku && ku.value.digitalSignature !== true) continue;
|
|
1879
|
+
// A delegate carrying a critical extension this code does not process is
|
|
1880
|
+
// unusable, the same fail-closed rule the path validator applies to any cert
|
|
1881
|
+
// (RFC 5280 sec. 6.1.4(o)) -- an unknown critical constraint must not be ignored
|
|
1882
|
+
// while its key is trusted to authenticate revocation status. A RECOGNIZED
|
|
1883
|
+
// critical extension whose value is malformed is likewise rejected via the
|
|
1884
|
+
// shared structure validation, exactly as the path validator rejects it.
|
|
1885
|
+
if (unrecognizedCriticalExtension(rc, false)) continue;
|
|
1886
|
+
if (validateCriticalExtensionStructure(rc)) continue;
|
|
1887
|
+
// The delegate MUST carry id-pkix-ocsp-nocheck (RFC 6960 sec. 4.2.2.2.1): a
|
|
1888
|
+
// transport-free checker cannot otherwise confirm the responder certificate has
|
|
1889
|
+
// not itself been revoked, and a revoked OCSP-signing certificate would keep
|
|
1890
|
+
// signing "good" until its notAfter. A deployment that instead supplies the
|
|
1891
|
+
// responder's own status opts in through a future checker; absent nocheck, fail
|
|
1892
|
+
// closed (unknown) rather than trust an unvalidated responder.
|
|
1893
|
+
if (!findExt(rc, OID_OCSP_NOCHECK)) continue;
|
|
1894
|
+
return ocspResponderSpki(rc, issuer);
|
|
1895
|
+
}
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* @primitive pki.path.ocspChecker
|
|
1901
|
+
* @signature pki.path.ocspChecker(responses) -> RevocationChecker
|
|
1902
|
+
* @since 0.1.32
|
|
1903
|
+
* @status experimental
|
|
1904
|
+
* @spec RFC 6960
|
|
1905
|
+
* @related pki.path.validate, pki.schema.ocsp.parseResponse, pki.path.crlChecker
|
|
1906
|
+
*
|
|
1907
|
+
* Build an OCSP-backed `RevocationChecker` for `pki.path.validate`'s
|
|
1908
|
+
* `revocationChecker` option from a set of pre-fetched OCSP responses (DER/PEM
|
|
1909
|
+
* or already-parsed). For each certificate it locates a SingleResponse whose
|
|
1910
|
+
* CertID binds this cert's serial to its issuer -- recomputing `issuerNameHash`
|
|
1911
|
+
* and `issuerKeyHash` under the CertID's own hashAlgorithm (SHA-1 or SHA-2), so
|
|
1912
|
+
* a response using either matches -- confirms the responder is authorized (the
|
|
1913
|
+
* issuing CA directly, or a valid CA-issued delegate bearing both id-kp-OCSPSigning
|
|
1914
|
+
* and id-pkix-ocsp-nocheck), verifies the response signature over
|
|
1915
|
+
* `tbsResponseDataBytes`, checks currency
|
|
1916
|
+
* (`thisUpdate`/`nextUpdate`), and reports `{ status: "good"|"revoked"|
|
|
1917
|
+
* "unknown" }`. A wrong-issuer CertID, an unauthorized responder, a stale,
|
|
1918
|
+
* not-yet-valid, nextUpdate-less, non-successful, or unverifiable response
|
|
1919
|
+
* yields `unknown`, which the validator fails closed unless `softFail` is set;
|
|
1920
|
+
* a `revoked` status surfaces its `revocationReason`. It is transport-free: the
|
|
1921
|
+
* caller supplies bytes it collected (an OCSP fetch or a stapled response), so
|
|
1922
|
+
* nonce anti-replay is the live client's responsibility and the residual replay
|
|
1923
|
+
* defense is the `thisUpdate`/`nextUpdate` currency window.
|
|
1924
|
+
*
|
|
1925
|
+
* @example
|
|
1926
|
+
* var checker = pki.path.ocspChecker([]); // no responses -> every cert is "unknown"
|
|
1927
|
+
* typeof checker.check; // "function"
|
|
1928
|
+
*/
|
|
1929
|
+
function ocspChecker(responses) {
|
|
1930
|
+
var parsed = (responses || []).map(function (r) { return (r && r.responseStatus) ? r : ocsp.parseResponse(r); });
|
|
1931
|
+
return {
|
|
1932
|
+
check: async function (cert, issuer, ctx) {
|
|
1933
|
+
var time = ctx.time;
|
|
1934
|
+
var historical = ctx.historicalMode === true;
|
|
1935
|
+
// Issuer DN candidates to match the CertID against (RFC 6960 sec. 4.1.1 names
|
|
1936
|
+
// the checked cert's issuer field; a response MAY instead carry the issuer
|
|
1937
|
+
// certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
|
|
1938
|
+
var issuerNameCandidates = [cert.issuer.bytes];
|
|
1939
|
+
function addNameCandidate(nm) {
|
|
1940
|
+
if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
|
|
1941
|
+
}
|
|
1942
|
+
if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
|
|
1943
|
+
addNameCandidate(issuer.workingIssuerName);
|
|
1944
|
+
var issuerKeyBits;
|
|
1945
|
+
try { issuerKeyBits = ocspKeyValue(issuer.workingPublicKey); }
|
|
1946
|
+
catch (_e) { return { status: "unknown", reason: "the issuer public key could not be read to recompute the OCSP CertID" }; }
|
|
1947
|
+
|
|
1948
|
+
// A serial is revoked if ANY authoritative, verified, current response says
|
|
1949
|
+
// so -- a clean response must never shadow a revoking one (the crlChecker
|
|
1950
|
+
// fail-closed law). "good" needs at least one authoritative match; every
|
|
1951
|
+
// other outcome is undetermined.
|
|
1952
|
+
var revokedResult = null;
|
|
1953
|
+
var sawGood = false;
|
|
1954
|
+
var sawUnknownStatus = false;
|
|
1955
|
+
|
|
1956
|
+
for (var k = 0; k < parsed.length; k++) {
|
|
1957
|
+
var resp = parsed[k];
|
|
1958
|
+
if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
|
|
1959
|
+
var br = resp.basicResponse;
|
|
1960
|
+
if (!br) continue;
|
|
1961
|
+
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
|
|
1962
|
+
if (!signerSpki) continue; // unauthorized responder
|
|
1963
|
+
if (!(await _verifyWithSpki(br.signatureAlgorithm, br.signature, signerSpki, br.tbsResponseDataBytes))) continue;
|
|
1964
|
+
// A critical responseExtension changes the meaning of the WHOLE response
|
|
1965
|
+
// and this code processes none -> the response is unusable.
|
|
1966
|
+
if (ocspHasCriticalExtension(br.responseExtensions)) continue;
|
|
1967
|
+
for (var s = 0; s < br.responses.length; s++) {
|
|
1968
|
+
var sr = br.responses[s];
|
|
1969
|
+
if (!(await ocspCertIdMatches(sr.certID, cert, issuerNameCandidates, issuerKeyBits))) continue; // not about this cert
|
|
1970
|
+
if (ocspHasCriticalExtension(sr.singleExtensions)) continue; // a critical per-response extension this code cannot process -> skip
|
|
1971
|
+
if (sr.thisUpdate > time) continue; // not yet valid
|
|
1972
|
+
if (!sr.nextUpdate || sr.nextUpdate < time) continue; // no bounded validity / stale -> fail closed
|
|
1973
|
+
var st = sr.certStatus;
|
|
1974
|
+
if (st.type === "revoked") {
|
|
1975
|
+
// A revocation is effective as of its revocationTime. Present-time
|
|
1976
|
+
// validation revokes regardless (a future revocationTime is
|
|
1977
|
+
// post-dating/skew, never "good"); only an EXPLICIT historical
|
|
1978
|
+
// validation defers a strictly-future revocation.
|
|
1979
|
+
if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { sawGood = true; }
|
|
1980
|
+
else {
|
|
1981
|
+
revokedResult = {
|
|
1982
|
+
status: "revoked",
|
|
1983
|
+
revocationReason: st.revocationReason || null,
|
|
1984
|
+
reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : ""),
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
} else if (st.type === "good") {
|
|
1988
|
+
sawGood = true;
|
|
1989
|
+
} else {
|
|
1990
|
+
sawUnknownStatus = true; // an explicit responder "unknown"
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (revokedResult) return revokedResult;
|
|
1995
|
+
if (sawGood) return { status: "good" };
|
|
1996
|
+
return {
|
|
1997
|
+
status: "unknown",
|
|
1998
|
+
reason: sawUnknownStatus
|
|
1999
|
+
? "the OCSP responder reported certStatus unknown for this certificate"
|
|
2000
|
+
: "no authoritative, current, in-scope OCSP response covers this certificate",
|
|
2001
|
+
};
|
|
2002
|
+
},
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1602
2006
|
module.exports = {
|
|
1603
2007
|
validate: validate,
|
|
1604
2008
|
crlChecker: crlChecker,
|
|
2009
|
+
ocspChecker: ocspChecker,
|
|
1605
2010
|
};
|