@blamejs/pki 0.7.24 → 0.7.26
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 +34 -0
- package/README.md +3 -3
- package/lib/attrcert-sign.js +3 -3
- package/lib/cmp-build.js +3 -3
- package/lib/cms-sign.js +3 -3
- package/lib/crl-sign.js +2 -2
- package/lib/crmf-sign.js +1 -1
- package/lib/guard-time.js +20 -0
- package/lib/lint.js +538 -14
- package/lib/ocsp.js +80 -5
- package/lib/pki-build.js +1 -1
- package/lib/schema-crl.js +5 -0
- package/lib/schema-ocsp.js +76 -2
- package/lib/tsp-sign.js +1 -1
- package/lib/x509-sign.js +3 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/lint.js
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
* @module pki.lint
|
|
6
6
|
* @nav Tooling
|
|
7
7
|
* @title Lint
|
|
8
|
-
* @fullname Certificate
|
|
9
|
-
* requirements
|
|
8
|
+
* @fullname Certificate, revocation-list and OCSP-response linting against RFC 5280, RFC 6960
|
|
9
|
+
* and CA/Browser Forum requirements
|
|
10
10
|
* @intro The LINTING engine, the zlint / pkilint of JavaScript. It walks an ALREADY-PARSED
|
|
11
|
-
* certificate
|
|
12
|
-
* `pki.schema.crl.parse`, whose extension values it
|
|
13
|
-
* decoders) and emits graded, advisory FINDINGS: each with
|
|
14
|
-
* (`fatal` > `error` > `warn` > `notice` > `pass`), a source, a
|
|
15
|
-
* a human message. It ships the RFC 5280 certificate profile, the
|
|
16
|
-
* revocation-list profile, the
|
|
11
|
+
* certificate, certificate revocation list or OCSP response (from `pki.schema.x509.parse`,
|
|
12
|
+
* `pki.schema.crl.parse` or `pki.schema.ocsp.parseResponse`, whose extension values it
|
|
13
|
+
* decodes with the shared RFC 5280 decoders) and emits graded, advisory FINDINGS: each with
|
|
14
|
+
* a stable id, a severity (`fatal` > `error` > `warn` > `notice` > `pass`), a source, a
|
|
15
|
+
* spec-clause citation, and a human message. It ships the RFC 5280 certificate profile, the
|
|
16
|
+
* RFC 5280 section 5 revocation-list profile, the RFC 6960 OCSP response profile with the
|
|
17
|
+
* RFC 5019 lightweight rows, the post-quantum certificate profiles for ML-KEM, ML-DSA and
|
|
17
18
|
* SLH-DSA, and a representative CA/Browser Forum TLS Baseline Requirements subset.
|
|
18
19
|
*
|
|
19
20
|
* Unlike every other toolkit entry, the lint data path never throws. A linter surveys a
|
|
@@ -23,8 +24,8 @@
|
|
|
23
24
|
* unknown profile, an out-of-range severity threshold, or a wrong-type input), which
|
|
24
25
|
* raises a typed `LintError`. This deliberate inversion of the toolkit's fail-closed-throw
|
|
25
26
|
* posture is what lets an operator lint a whole directory without a try/catch per file.
|
|
26
|
-
* @spec RFC 5280, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS Baseline Requirements
|
|
27
|
-
* @card Lint a certificate or
|
|
27
|
+
* @spec RFC 5280, RFC 6960, RFC 5019, RFC 9654, RFC 9881, RFC 9909, RFC 9935, CA/Browser Forum TLS Baseline Requirements
|
|
28
|
+
* @card Lint a certificate, CRL or OCSP response against RFC 5280, RFC 6960 + CABF TLS BR, in pure JS.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
var frameworkError = require("./framework-error");
|
|
@@ -33,6 +34,7 @@ var guard = require("./guard-all");
|
|
|
33
34
|
var oid = require("./oid");
|
|
34
35
|
var x509 = require("./schema-x509");
|
|
35
36
|
var crlSchema = require("./schema-crl");
|
|
37
|
+
var ocspSchema = require("./schema-ocsp");
|
|
36
38
|
var pkix = require("./schema-pkix");
|
|
37
39
|
var schema = require("./schema-engine");
|
|
38
40
|
var path = require("./path-validate");
|
|
@@ -261,6 +263,13 @@ function _criticalityRule(name, id, severity, citation, message) {
|
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
var RFC5280_RULES = [
|
|
266
|
+
{
|
|
267
|
+
id: "lint/rfc5280/signature-empty", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.1.3",
|
|
268
|
+
/** @internal The strict parser admits an empty BIT STRING, since one is well-formed DER; a
|
|
269
|
+
* signatureValue that contains no signature is not the field section 4.1.1.3 defines. */
|
|
270
|
+
message: "the signatureValue contains no signature",
|
|
271
|
+
check: function (cert) { return cert.signatureValue.bytes.length === 0 ? true : null; },
|
|
272
|
+
},
|
|
264
273
|
{
|
|
265
274
|
id: "lint/rfc5280/serial-not-positive", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.2.2",
|
|
266
275
|
message: "the certificate serialNumber must be a positive integer",
|
|
@@ -832,6 +841,11 @@ function _criticalityRows(list, table, what) {
|
|
|
832
841
|
* and a present-but-empty revokedCertificates (sec. 5.1.2.6, `crl/bad-revoked-certificates`). Each
|
|
833
842
|
* reaches a caller as the engine's fatal lint/unparseable carrying that code. */
|
|
834
843
|
var RFC5280_CRL_RULES = [
|
|
844
|
+
{
|
|
845
|
+
id: "lint/rfc5280-crl/signature-empty", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.1.3",
|
|
846
|
+
message: "the signatureValue contains no signature",
|
|
847
|
+
check: function (crl) { return crl.signatureValue.bytes.length === 0 ? true : null; },
|
|
848
|
+
},
|
|
835
849
|
{
|
|
836
850
|
id: "lint/rfc5280-crl/next-update-missing", severity: "error", source: "rfc5280-crl", citation: "RFC 5280 5.1.2.5",
|
|
837
851
|
message: "a conforming CRL issuer includes the nextUpdate field in every CRL",
|
|
@@ -1281,6 +1295,398 @@ var RFC5280_CRL_RULES = [
|
|
|
1281
1295
|
var CRL_PROFILES = Object.assign(Object.create(null), { "rfc5280-crl": RFC5280_CRL_RULES });
|
|
1282
1296
|
var ALL_CRL_RULES = RFC5280_CRL_RULES;
|
|
1283
1297
|
|
|
1298
|
+
|
|
1299
|
+
/** @internal The extensions RFC 6960 and RFC 9654 place on a response, by the list each belongs
|
|
1300
|
+
* in. The nonce and the extended revoked definition are response extensions; the archive cutoff and
|
|
1301
|
+
* the CRL references are single extensions, and sec. 4.4.5 carries every RFC 5280 sec. 5.3 CRL
|
|
1302
|
+
* entry extension into a SingleResponse as well. Three more are defined for REQUESTS only. */
|
|
1303
|
+
var OCSP_RESPONSE_KNOWN = Object.create(null);
|
|
1304
|
+
["ocspNonce", "ocspExtendedRevoke"].forEach(function (n) { OCSP_RESPONSE_KNOWN[oid.byName(n)] = true; });
|
|
1305
|
+
var OCSP_SINGLE_KNOWN = Object.create(null);
|
|
1306
|
+
["ocspArchiveCutoff", "ocspCrl", "reasonCode", "invalidityDate", "certificateIssuer"]
|
|
1307
|
+
.forEach(function (n) { OCSP_SINGLE_KNOWN[oid.byName(n)] = true; });
|
|
1308
|
+
var OCSP_REQUEST_ONLY = Object.create(null);
|
|
1309
|
+
[["ocspResponse", "RFC 6960 4.4.3"], ["ocspServiceLocator", "RFC 6960 4.4.6"], ["ocspPrefSigAlgs", "RFC 6960 4.4.7"]]
|
|
1310
|
+
.forEach(function (r) { OCSP_REQUEST_ONLY[oid.byName(r[0])] = r[1]; });
|
|
1311
|
+
/** @internal Only the criticality a clause states. Sec. 4.4.8 forbids a critical extended revoked
|
|
1312
|
+
* definition; sec. 4.4.5 carries the RFC 5280 sec. 5.3 rules for the three entry extensions. RFC
|
|
1313
|
+
* 6960 states nothing about the criticality of the nonce, the archive cutoff or the CRL references,
|
|
1314
|
+
* so they are absent here. */
|
|
1315
|
+
var OCSP_RESPONSE_CRITICALITY = Object.create(null);
|
|
1316
|
+
OCSP_RESPONSE_CRITICALITY[oid.byName("ocspExtendedRevoke")] = false;
|
|
1317
|
+
var OCSP_SINGLE_CRITICALITY = CRL_ENTRY_CRITICALITY;
|
|
1318
|
+
|
|
1319
|
+
var OID_OCSP_NONCE = oid.byName("ocspNonce");
|
|
1320
|
+
var OID_OCSP_EXTENDED_REVOKE = oid.byName("ocspExtendedRevoke");
|
|
1321
|
+
var OID_OCSP_ARCHIVE_CUTOFF = oid.byName("ocspArchiveCutoff");
|
|
1322
|
+
var OID_OCSP_CRL = oid.byName("ocspCrl");
|
|
1323
|
+
var OID_REASON_CODE = oid.byName("reasonCode");
|
|
1324
|
+
|
|
1325
|
+
/** @internal How each single extension the OCSP parser leaves opaque is read: reasonCode and
|
|
1326
|
+
* invalidityDate through the CRL parser's own entry-extension reader, so the linter and the CRL
|
|
1327
|
+
* parser cannot disagree about what a well-formed one is, and certificateIssuer through the shared
|
|
1328
|
+
* GeneralNames walk the CRL rows use. The parser decodes the nonce, the archive cutoff and the CRL
|
|
1329
|
+
* references itself and refuses a malformed one, so an entry for any of those would be dead. */
|
|
1330
|
+
var OCSP_SINGLE_VALUE_READER = Object.create(null);
|
|
1331
|
+
OCSP_SINGLE_VALUE_READER[OID_REASON_CODE] = function (bytes) {
|
|
1332
|
+
crlSchema.decodeExtension({ oid: OID_REASON_CODE, name: "reasonCode", critical: false, value: bytes });
|
|
1333
|
+
};
|
|
1334
|
+
OCSP_SINGLE_VALUE_READER[oid.byName("invalidityDate")] = function (bytes) {
|
|
1335
|
+
crlSchema.decodeExtension({ oid: oid.byName("invalidityDate"), name: "invalidityDate", critical: false, value: bytes });
|
|
1336
|
+
};
|
|
1337
|
+
OCSP_SINGLE_VALUE_READER[oid.byName("certificateIssuer")] = CRL_ENTRY_VALUE_READER[oid.byName("certificateIssuer")];
|
|
1338
|
+
var OCSP_RESPONSE_VALUE_READER = Object.create(null);
|
|
1339
|
+
/** @internal Read through the codec's NULL reader, which refuses content octets as well as a wrong
|
|
1340
|
+
* tag; the decoder alone frames `05 01 00` as a NULL carrying one octet. */
|
|
1341
|
+
OCSP_RESPONSE_VALUE_READER[OID_OCSP_EXTENDED_REVOKE] = function (bytes) {
|
|
1342
|
+
asn1.read.nullValue(asn1.decode(bytes));
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
function _ocspBasic(resp) { return resp.basicResponse; }
|
|
1346
|
+
/** @internal The parser refuses an empty `responses`, so the list is never empty here. */
|
|
1347
|
+
function _ocspResponses(resp) { return _ocspBasic(resp).responses; }
|
|
1348
|
+
function _ocspResponseExts(resp) { return _ocspBasic(resp).responseExtensions || []; }
|
|
1349
|
+
function _ocspSingleExts(sr) { return sr.singleExtensions || []; }
|
|
1350
|
+
function _extIn(list, dotted) {
|
|
1351
|
+
for (var i = 0; i < list.length; i++) { if (list[i].oid === dotted) return list[i]; }
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
/** @internal The revocation reason a single extension carries, read with the CRL parser's reader,
|
|
1355
|
+
* or null when the extension is absent or does not read (the value-syntax row reports that). */
|
|
1356
|
+
function _singleReasonCode(sr) {
|
|
1357
|
+
var e = _extIn(_ocspSingleExts(sr), OID_REASON_CODE);
|
|
1358
|
+
if (!e || !Buffer.isBuffer(e.value)) return null;
|
|
1359
|
+
// allow:swallow-unverified a value the CRL reader refuses carries no reason; the value-syntax row reports it
|
|
1360
|
+
try { return crlSchema.decodeExtension(e).value; }
|
|
1361
|
+
catch (_e) { return null; }
|
|
1362
|
+
}
|
|
1363
|
+
/** @internal RFC 6960 sec. 2.2 gives a "revoked" answer for a non-issued certificate a fixed shape:
|
|
1364
|
+
* the reason certificateHold and the revocation time January 1, 1970. That shape is how a response
|
|
1365
|
+
* says it is answering for a certificate that was never issued, and the rows that hold such an
|
|
1366
|
+
* answer to the rest of sec. 2.2 recognize it by exactly those two values. */
|
|
1367
|
+
function _isNonIssuedShape(sr) {
|
|
1368
|
+
var st = sr.certStatus;
|
|
1369
|
+
return !!st && st.type === "revoked" && st.revocationReason === "certificateHold" &&
|
|
1370
|
+
guard.time.isDate(st.revocationTime) && guard.time.instantOf(st.revocationTime) === 0;
|
|
1371
|
+
}
|
|
1372
|
+
/** @internal The rows in one list that carry a per-response index, so a finding names which
|
|
1373
|
+
* SingleResponse it is about the way the CRL entry rows name their entry. */
|
|
1374
|
+
function _perResponse(resp, fn) {
|
|
1375
|
+
var rows = [];
|
|
1376
|
+
_ocspResponses(resp).forEach(function (sr, i) {
|
|
1377
|
+
var out = fn(sr, i);
|
|
1378
|
+
if (out == null) return;
|
|
1379
|
+
(Array.isArray(out) ? out : [out]).forEach(function (row) { row.context.response = i; rows.push(row); });
|
|
1380
|
+
});
|
|
1381
|
+
return rows.length ? rows : null;
|
|
1382
|
+
}
|
|
1383
|
+
/** @internal A CertID as one lookup key, every field included, so two SingleResponses name the same
|
|
1384
|
+
* certificate under the same hash exactly when their keys match. Keyed rather than compared
|
|
1385
|
+
* pairwise: a response lists as many SingleResponses as its author chooses, and this verb exists to
|
|
1386
|
+
* survey ones that arrived from elsewhere, so a pairwise scan would be quadratic in that choice. */
|
|
1387
|
+
function _certIdKey(c) {
|
|
1388
|
+
return c.hashAlgorithm.algorithm + "|" + c.issuerNameHash.toString("hex") + "|" + c.issuerKeyHash.toString("hex") + "|" + c.serialNumberHex;
|
|
1389
|
+
}
|
|
1390
|
+
function _statusKey(st) {
|
|
1391
|
+
return st.type + (st.type === "revoked" ? ":" + guard.time.instantOf(st.revocationTime) + ":" + (st.revocationReason || "") : "");
|
|
1392
|
+
}
|
|
1393
|
+
/** @internal The placement rows share one shape: an extension found in the list the specification
|
|
1394
|
+
* does not define it for. `where` names the list it was found in; `context.placed` says where the
|
|
1395
|
+
* specification puts it. */
|
|
1396
|
+
function _placementRow(name, placed, where) {
|
|
1397
|
+
return { context: { extension: name, placed: placed, was: where } };
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** @internal Four sec. 4.2 rules have no row because the strict parser refuses them first and a rule
|
|
1401
|
+
* for them would be dead: a version other than v1 (sec. 4.2.2.3, `ocsp/bad-version`), an undefined
|
|
1402
|
+
* CRLReason value (`ocsp/bad-revocation-reason`), an empty extension list (`ocsp/bad-extensions`)
|
|
1403
|
+
* and a repeated extension (`ocsp/duplicate-extension`); so are an empty `responses`
|
|
1404
|
+
* (`ocsp/bad-responses`), a nonce outside 1..128 octets (`ocsp/bad-nonce`), a malformed archive
|
|
1405
|
+
* cutoff or CrlID, a responseStatus that disagrees with responseBytes, and every time-format rule of
|
|
1406
|
+
* RFC 5019 sec. 2.2.4, which the DER codec enforces. Each reaches a caller as the engine's fatal
|
|
1407
|
+
* lint/unparseable carrying that code. */
|
|
1408
|
+
var RFC6960_OCSP_RULES = [
|
|
1409
|
+
{
|
|
1410
|
+
id: "lint/rfc6960/signature-empty", severity: "error", source: "rfc6960", citation: "RFC 6960 2.2",
|
|
1411
|
+
message: "a definitive response shall be digitally signed; the signature BIT STRING is empty",
|
|
1412
|
+
check: function (resp) { return _ocspBasic(resp).signature.length === 0 ? true : null; },
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
id: "lint/rfc6960/update-times-inverted", severity: "error", source: "rfc6960", citation: "RFC 6960 2.4 / 4.2.2.1",
|
|
1416
|
+
/** @internal Sec. 4.2.2.1 defines the two as a validity interval and says it corresponds to the
|
|
1417
|
+
* CRL's, whose inversion the CRL profile grades the same way; an interval whose end precedes
|
|
1418
|
+
* its start is a fault of definition, like an extension on the wrong list, rather than a
|
|
1419
|
+
* reading, and pki.ocsp.sign refuses to write one. */
|
|
1420
|
+
message: "a SingleResponse's thisUpdate must not follow its nextUpdate",
|
|
1421
|
+
check: function (resp) {
|
|
1422
|
+
return _perResponse(resp, function (sr) {
|
|
1423
|
+
// allow:nan-date-comparison-unguarded -- both are codec-parsed OCSP times (asn1 readTime rejects a NaN instant).
|
|
1424
|
+
if (sr.nextUpdate === null) return null;
|
|
1425
|
+
return guard.time.instantOf(sr.thisUpdate) > guard.time.instantOf(sr.nextUpdate) ? { context: {} } : null;
|
|
1426
|
+
});
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
id: "lint/rfc6960/produced-before-this-update", severity: "notice", source: "rfc6960", citation: "RFC 6960 2.4 / 2.5",
|
|
1431
|
+
/** @internal producedAt is the time the responder signed, thisUpdate the most recent time the
|
|
1432
|
+
* status was known to be correct; a response signed before that time certifies status it did
|
|
1433
|
+
* not yet know. No clause forbids it, so this is advisory. */
|
|
1434
|
+
message: "producedAt is earlier than a SingleResponse's thisUpdate, so the response was signed before the status it certifies was known",
|
|
1435
|
+
check: function (resp) {
|
|
1436
|
+
var produced = _ocspBasic(resp).producedAt;
|
|
1437
|
+
return _perResponse(resp, function (sr) {
|
|
1438
|
+
// allow:nan-date-comparison-unguarded -- both are codec-parsed OCSP times (asn1 readTime rejects a NaN instant).
|
|
1439
|
+
return guard.time.instantOf(produced) < guard.time.instantOf(sr.thisUpdate) ? { context: {} } : null;
|
|
1440
|
+
});
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
id: "lint/rfc6960/certs-present-but-empty", severity: "warn", source: "rfc6960", citation: "RFC 6960 4.2.1",
|
|
1445
|
+
message: "certs should be absent when no certificates are included",
|
|
1446
|
+
check: function (resp) { var bs = _ocspBasic(resp); return bs.certsPresent === true && bs.certs.length === 0 ? true : null; },
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
id: "lint/rfc6960/response-extension-criticality", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.8",
|
|
1450
|
+
message: "a response extension is marked with a criticality the profile does not allow",
|
|
1451
|
+
check: function (resp) {
|
|
1452
|
+
var rows = _criticalityRows(_ocspResponseExts(resp), OCSP_RESPONSE_CRITICALITY, "responseExtensions");
|
|
1453
|
+
return rows.length ? rows : null;
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
id: "lint/rfc6960/single-extension-criticality", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.5 / RFC 5280 5.3.1 / 5.3.2 / 5.3.3",
|
|
1458
|
+
message: "a single extension is marked with a criticality the profile does not allow",
|
|
1459
|
+
check: function (resp) {
|
|
1460
|
+
return _perResponse(resp, function (sr) { return _criticalityRows(_ocspSingleExts(sr), OCSP_SINGLE_CRITICALITY, "singleExtensions"); });
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
id: "lint/rfc6960/extended-revoke-value-not-null", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.8",
|
|
1465
|
+
message: "the value of the extended revoked definition extension shall be NULL",
|
|
1466
|
+
check: function (resp) {
|
|
1467
|
+
var e = _extIn(_ocspResponseExts(resp), OID_OCSP_EXTENDED_REVOKE);
|
|
1468
|
+
return e && _valueSyntaxFault(e, OCSP_RESPONSE_KNOWN, OCSP_RESPONSE_VALUE_READER) !== null ? true : null;
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
id: "lint/rfc6960/extended-revoke-in-single-extensions", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.8",
|
|
1473
|
+
message: "the extended revoked definition extension must be placed in responseExtensions and must not appear in singleExtensions",
|
|
1474
|
+
check: function (resp) {
|
|
1475
|
+
return _perResponse(resp, function (sr) {
|
|
1476
|
+
return _extIn(_ocspSingleExts(sr), OID_OCSP_EXTENDED_REVOKE) ? _placementRow("ocspExtendedRevoke", "responseExtensions", "singleExtensions") : null;
|
|
1477
|
+
});
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
id: "lint/rfc6960/non-issued-without-extended-revoke", severity: "error", source: "rfc6960", citation: "RFC 6960 2.2",
|
|
1482
|
+
message: "a revoked answer for a non-issued certificate (certificateHold at January 1, 1970) must be accompanied by the extended revoked definition response extension",
|
|
1483
|
+
check: function (resp) {
|
|
1484
|
+
if (_extIn(_ocspResponseExts(resp), OID_OCSP_EXTENDED_REVOKE)) return null;
|
|
1485
|
+
return _perResponse(resp, function (sr) { return _isNonIssuedShape(sr) ? { context: {} } : null; });
|
|
1486
|
+
},
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
id: "lint/rfc6960/non-issued-with-crl-extensions", severity: "error", source: "rfc6960", citation: "RFC 6960 2.2",
|
|
1490
|
+
message: "a revoked answer for a non-issued certificate must not include a CRL references extension or any CRL entry extension",
|
|
1491
|
+
check: function (resp) {
|
|
1492
|
+
return _perResponse(resp, function (sr) {
|
|
1493
|
+
if (!_isNonIssuedShape(sr)) return null;
|
|
1494
|
+
var rows = [];
|
|
1495
|
+
_ocspSingleExts(sr).forEach(function (e) {
|
|
1496
|
+
if (e.oid === OID_OCSP_CRL || CRL_ENTRY_CRITICALITY[e.oid] !== undefined) rows.push({ context: { extension: e.name } });
|
|
1497
|
+
});
|
|
1498
|
+
return rows.length ? rows : null;
|
|
1499
|
+
});
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
id: "lint/rfc6960/archive-cutoff-in-response-extensions", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.4",
|
|
1504
|
+
message: "an archive cutoff shall be provided as a singleExtensions extension",
|
|
1505
|
+
check: function (resp) {
|
|
1506
|
+
return _extIn(_ocspResponseExts(resp), OID_OCSP_ARCHIVE_CUTOFF) ? _placementRow("ocspArchiveCutoff", "singleExtensions", "responseExtensions") : null;
|
|
1507
|
+
},
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
id: "lint/rfc6960/crl-references-in-response-extensions", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.2",
|
|
1511
|
+
message: "the CRL references extension is specified as a singleExtensions extension",
|
|
1512
|
+
check: function (resp) {
|
|
1513
|
+
return _extIn(_ocspResponseExts(resp), OID_OCSP_CRL) ? _placementRow("ocspCrl", "singleExtensions", "responseExtensions") : null;
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
id: "lint/rfc6960/nonce-in-single-extensions", severity: "error", source: "rfc6960", citation: "RFC 9654 2.1",
|
|
1518
|
+
message: "in a response the nonce is included as one of the responseExtensions",
|
|
1519
|
+
check: function (resp) {
|
|
1520
|
+
return _perResponse(resp, function (sr) {
|
|
1521
|
+
return _extIn(_ocspSingleExts(sr), OID_OCSP_NONCE) ? _placementRow("ocspNonce", "responseExtensions", "singleExtensions") : null;
|
|
1522
|
+
});
|
|
1523
|
+
},
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
id: "lint/rfc6960/request-extension-in-response", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.3 / 4.4.6 / 4.4.7",
|
|
1527
|
+
message: "an extension defined for OCSP requests only is carried on a response",
|
|
1528
|
+
check: function (resp) {
|
|
1529
|
+
var rows = [];
|
|
1530
|
+
_ocspResponseExts(resp).forEach(function (e) {
|
|
1531
|
+
if (OCSP_REQUEST_ONLY[e.oid] !== undefined) rows.push({ context: { extension: e.name, defined: OCSP_REQUEST_ONLY[e.oid], was: "responseExtensions" } });
|
|
1532
|
+
});
|
|
1533
|
+
var per = _perResponse(resp, function (sr) {
|
|
1534
|
+
var out = [];
|
|
1535
|
+
_ocspSingleExts(sr).forEach(function (e) {
|
|
1536
|
+
if (OCSP_REQUEST_ONLY[e.oid] !== undefined) out.push({ context: { extension: e.name, defined: OCSP_REQUEST_ONLY[e.oid], was: "singleExtensions" } });
|
|
1537
|
+
});
|
|
1538
|
+
return out.length ? out : null;
|
|
1539
|
+
});
|
|
1540
|
+
if (per) per.forEach(function (row) { rows.push(row); });
|
|
1541
|
+
return rows.length ? rows : null;
|
|
1542
|
+
},
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
id: "lint/rfc6960/archive-cutoff-after-produced-at", severity: "notice", source: "rfc6960", citation: "RFC 6960 4.4.4",
|
|
1546
|
+
/** @internal The archive cutoff is defined as producedAt minus the responder's retention
|
|
1547
|
+
* interval, so a cutoff after producedAt states a negative interval. Stated by definition
|
|
1548
|
+
* rather than as a requirement, so advisory. */
|
|
1549
|
+
message: "an archive cutoff later than producedAt implies a negative retention interval",
|
|
1550
|
+
check: function (resp) {
|
|
1551
|
+
var produced = _ocspBasic(resp).producedAt;
|
|
1552
|
+
return _perResponse(resp, function (sr) {
|
|
1553
|
+
var e = _extIn(_ocspSingleExts(sr), OID_OCSP_ARCHIVE_CUTOFF);
|
|
1554
|
+
// allow:nan-date-comparison-unguarded -- both are codec-parsed OCSP times (the parser refuses an archive cutoff that is not a GeneralizedTime).
|
|
1555
|
+
if (!e) return null;
|
|
1556
|
+
return guard.time.instantOf(e.archiveCutoff) > guard.time.instantOf(produced) ? { context: {} } : null;
|
|
1557
|
+
});
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
id: "lint/rfc6960/extension-value-syntax", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.5 / 4.4.8",
|
|
1562
|
+
message: "a recognized extension's value does not decode as the structure its identifier names",
|
|
1563
|
+
check: function (resp) {
|
|
1564
|
+
var rows = [];
|
|
1565
|
+
_ocspResponseExts(resp).forEach(function (e) {
|
|
1566
|
+
var why = _valueSyntaxFault(e, OCSP_RESPONSE_KNOWN, OCSP_RESPONSE_VALUE_READER);
|
|
1567
|
+
if (why !== null) rows.push({ context: { extension: e.name, scope: "responseExtensions", problem: why } });
|
|
1568
|
+
});
|
|
1569
|
+
var per = _perResponse(resp, function (sr) {
|
|
1570
|
+
var out = [];
|
|
1571
|
+
_ocspSingleExts(sr).forEach(function (e) {
|
|
1572
|
+
var why = _valueSyntaxFault(e, OCSP_SINGLE_KNOWN, OCSP_SINGLE_VALUE_READER);
|
|
1573
|
+
if (why !== null) out.push({ context: { extension: e.name, scope: "singleExtensions", problem: why } });
|
|
1574
|
+
});
|
|
1575
|
+
return out.length ? out : null;
|
|
1576
|
+
});
|
|
1577
|
+
if (per) per.forEach(function (row) { rows.push(row); });
|
|
1578
|
+
return rows.length ? rows : null;
|
|
1579
|
+
},
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
id: "lint/rfc6960/unknown-critical-extension", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4 / RFC 5019 2.2.1",
|
|
1583
|
+
/** @internal RFC 6960 adopts the RFC 5280 extension model, under which a consumer that cannot
|
|
1584
|
+
* process a critical extension must not use the structure, and RFC 5019 lets a client ignore
|
|
1585
|
+
* only an unrecognized NON-critical response extension. The recognized set is the one this
|
|
1586
|
+
* profile reads, per list. */
|
|
1587
|
+
message: "a critical extension the profile does not recognize is present",
|
|
1588
|
+
check: function (resp) {
|
|
1589
|
+
var rows = [];
|
|
1590
|
+
_ocspResponseExts(resp).forEach(function (e) {
|
|
1591
|
+
if (e.critical === true && OCSP_RESPONSE_KNOWN[e.oid] === undefined) rows.push({ context: { extension: e.name || e.oid, scope: "responseExtensions" } });
|
|
1592
|
+
});
|
|
1593
|
+
var per = _perResponse(resp, function (sr) {
|
|
1594
|
+
var out = [];
|
|
1595
|
+
_ocspSingleExts(sr).forEach(function (e) {
|
|
1596
|
+
if (e.critical === true && OCSP_SINGLE_KNOWN[e.oid] === undefined) out.push({ context: { extension: e.name || e.oid, scope: "singleExtensions" } });
|
|
1597
|
+
});
|
|
1598
|
+
return out.length ? out : null;
|
|
1599
|
+
});
|
|
1600
|
+
if (per) per.forEach(function (row) { rows.push(row); });
|
|
1601
|
+
return rows.length ? rows : null;
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
/** @internal The two reason rules each have two carriers graded apart. The reasonCode single
|
|
1605
|
+
* extension IS the RFC 5280 sec. 5.3.1 extension, carried into a SingleResponse by sec. 4.4.5, so
|
|
1606
|
+
* its rows grade at that clause's strength. RevokedInfo's own revocationReason is a separate field
|
|
1607
|
+
* of the same CRLReason type, and the clause is applied to it by reading, so its rows are advisory. */
|
|
1608
|
+
{
|
|
1609
|
+
id: "lint/rfc6960/reason-code-unspecified", severity: "warn", source: "rfc6960", citation: "RFC 6960 4.4.5 / RFC 5280 5.3.1",
|
|
1610
|
+
message: "a reasonCode single extension of unspecified(0) should be absent instead",
|
|
1611
|
+
check: function (resp) { return _perResponse(resp, function (sr) { return _singleReasonCode(sr) === 0 ? { context: {} } : null; }); },
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
id: "lint/rfc6960/remove-from-crl-reason", severity: "error", source: "rfc6960", citation: "RFC 6960 4.4.5 / RFC 5280 5.3.1",
|
|
1615
|
+
message: "a reasonCode of removeFromCRL may only appear in a delta CRL; a status response is not one",
|
|
1616
|
+
check: function (resp) { return _perResponse(resp, function (sr) { return _singleReasonCode(sr) === 8 ? { context: {} } : null; }); },
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
id: "lint/rfc6960/revocation-reason-unspecified", severity: "notice", source: "rfc6960", citation: "RFC 6960 4.2.1 / RFC 5280 5.3.1",
|
|
1620
|
+
message: "a revocationReason of unspecified(0) states no reason; the reasonCode extension it mirrors should be absent instead",
|
|
1621
|
+
check: function (resp) {
|
|
1622
|
+
return _perResponse(resp, function (sr) {
|
|
1623
|
+
return sr.certStatus.type === "revoked" && sr.certStatus.revocationReason === "unspecified" ? { context: {} } : null;
|
|
1624
|
+
});
|
|
1625
|
+
},
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
id: "lint/rfc6960/revocation-reason-remove-from-crl", severity: "notice", source: "rfc6960", citation: "RFC 6960 4.2.1 / RFC 5280 5.3.1",
|
|
1629
|
+
message: "a revocationReason of removeFromCRL names a delta-CRL action, which a status response cannot state",
|
|
1630
|
+
check: function (resp) {
|
|
1631
|
+
return _perResponse(resp, function (sr) {
|
|
1632
|
+
return sr.certStatus.type === "revoked" && sr.certStatus.revocationReason === "removeFromCRL" ? { context: {} } : null;
|
|
1633
|
+
});
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
id: "lint/rfc6960/duplicate-cert-id", severity: "notice", source: "rfc6960", citation: "RFC 6960 4.2.2.3",
|
|
1638
|
+
/** @internal A response carries a SingleResponse per certificate. Two for the same CertID
|
|
1639
|
+
* answer one certificate twice, and when the two statuses differ a relying party cannot tell
|
|
1640
|
+
* which governs; the context says which case it is. No clause forbids the repeat, so advisory. */
|
|
1641
|
+
message: "two SingleResponses answer for the same CertID",
|
|
1642
|
+
check: function (resp) {
|
|
1643
|
+
var rows = [], first = Object.create(null);
|
|
1644
|
+
_ocspResponses(resp).forEach(function (sr, i) {
|
|
1645
|
+
var key = _certIdKey(sr.certID), seen = first[key];
|
|
1646
|
+
if (seen === undefined) { first[key] = { index: i, status: _statusKey(sr.certStatus) }; return; }
|
|
1647
|
+
rows.push({ context: { response: i, first: seen.index, conflicting: seen.status !== _statusKey(sr.certStatus) } });
|
|
1648
|
+
});
|
|
1649
|
+
return rows.length ? rows : null;
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
];
|
|
1653
|
+
|
|
1654
|
+
/** @internal Nothing in a response says which profile its responder follows, so the RFC 5019 rows
|
|
1655
|
+
* run only when that profile is selected and count as not applicable otherwise. */
|
|
1656
|
+
function _isLightweight(resp, ctx) { return ctx.profile === "rfc5019"; }
|
|
1657
|
+
|
|
1658
|
+
var RFC5019_OCSP_RULES = [
|
|
1659
|
+
{
|
|
1660
|
+
id: "lint/rfc5019/next-update-missing", severity: "error", source: "rfc5019", citation: "RFC 5019 2.2.4",
|
|
1661
|
+
message: "a lightweight-profile responder must always include nextUpdate",
|
|
1662
|
+
appliesTo: _isLightweight,
|
|
1663
|
+
check: function (resp) { return _perResponse(resp, function (sr) { return sr.nextUpdate == null ? { context: {} } : null; }); },
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
id: "lint/rfc5019/multiple-single-responses", severity: "warn", source: "rfc5019", citation: "RFC 5019 2.2.1",
|
|
1667
|
+
message: "a lightweight-profile response should include only one SingleResponse",
|
|
1668
|
+
appliesTo: _isLightweight,
|
|
1669
|
+
check: function (resp) { var n = _ocspResponses(resp).length; return n > 1 ? { context: { responses: n } } : null; },
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
id: "lint/rfc5019/response-extensions-present", severity: "warn", source: "rfc5019", citation: "RFC 5019 2.2.1",
|
|
1673
|
+
message: "a lightweight-profile responder should not include responseExtensions",
|
|
1674
|
+
appliesTo: _isLightweight,
|
|
1675
|
+
check: function (resp) { return _ocspBasic(resp).responseExtensions ? true : null; },
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
id: "lint/rfc5019/responder-id-by-name", severity: "notice", source: "rfc5019", citation: "RFC 5019 2.2.2",
|
|
1679
|
+
/** @internal A SHOULD conditioned on bandwidth being at issue, which the response cannot show,
|
|
1680
|
+
* so it grades as advisory rather than as the warning an unconditional SHOULD would draw. */
|
|
1681
|
+
message: "a lightweight-profile responder should use the byKey ResponderID where reducing bandwidth matters",
|
|
1682
|
+
appliesTo: _isLightweight,
|
|
1683
|
+
check: function (resp) { var rid = _ocspBasic(resp).responderID; return rid && rid.byName !== undefined ? true : null; },
|
|
1684
|
+
},
|
|
1685
|
+
];
|
|
1686
|
+
|
|
1687
|
+
var OCSP_PROFILES = Object.assign(Object.create(null), { "rfc6960": RFC6960_OCSP_RULES, "rfc5019": RFC5019_OCSP_RULES });
|
|
1688
|
+
var ALL_OCSP_RULES = RFC6960_OCSP_RULES.concat(RFC5019_OCSP_RULES);
|
|
1689
|
+
|
|
1284
1690
|
var PROFILES = Object.assign(Object.create(null), {
|
|
1285
1691
|
"rfc5280": RFC5280_RULES,
|
|
1286
1692
|
"rfc9881": RFC9881_RULES,
|
|
@@ -1294,6 +1700,7 @@ function _selectRules(profile) {
|
|
|
1294
1700
|
if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
|
|
1295
1701
|
if (!PROFILES[profile]) {
|
|
1296
1702
|
if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
|
|
1703
|
+
if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
|
|
1297
1704
|
throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\" (known: " + Object.keys(PROFILES).join(", ") + ")");
|
|
1298
1705
|
}
|
|
1299
1706
|
return PROFILES[profile];
|
|
@@ -1447,6 +1854,7 @@ function crl(input, opts) {
|
|
|
1447
1854
|
if (profile == null || profile === "all" || profile === "default") rules_ = ALL_CRL_RULES;
|
|
1448
1855
|
else if (CRL_PROFILES[profile]) rules_ = CRL_PROFILES[profile];
|
|
1449
1856
|
else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
|
|
1857
|
+
else if (OCSP_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is an OCSP profile; lint a response with pki.lint.ocsp");
|
|
1450
1858
|
else throw _cfg("lint/unknown-profile", "unknown CRL lint profile \"" + profile + "\" (known: " + Object.keys(CRL_PROFILES).join(", ") + ")");
|
|
1451
1859
|
|
|
1452
1860
|
var ingested = _ingestCrl(input);
|
|
@@ -1462,6 +1870,121 @@ function crl(input, opts) {
|
|
|
1462
1870
|
return report;
|
|
1463
1871
|
}
|
|
1464
1872
|
|
|
1873
|
+
var _OCSP_OPTS = Object.assign(Object.create(null), { profile: 1, severity: 1 });
|
|
1874
|
+
var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
|
|
1875
|
+
|
|
1876
|
+
/** @internal The door pki.ocsp.verify uses for a parsed response: a result the parser recorded is
|
|
1877
|
+
* re-read from the source it came from, which is the DER when the parser was fed bytes and the PEM
|
|
1878
|
+
* text when it was fed a string, and an object that merely carries a response's field names is
|
|
1879
|
+
* refused as config-time misuse rather than linted, since its fields could describe different
|
|
1880
|
+
* responses. Bytes and PEM take the fatal path below, never a throw. */
|
|
1881
|
+
function _ingestOcsp(input) {
|
|
1882
|
+
var der;
|
|
1883
|
+
if (Buffer.isBuffer(input)) der = input;
|
|
1884
|
+
else if (typeof input === "string") {
|
|
1885
|
+
try { der = ocspSchema.pemDecode(input); }
|
|
1886
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "pki.lint", message: "input is not a decodable PEM/DER OCSP response", context: { code: e.code } } }; }
|
|
1887
|
+
} else if (input !== null && typeof input === "object" && !guard.bytes.isByteSource(input)) {
|
|
1888
|
+
return { response: guard.parsed.fromTrustedSource(input, "ocspResponse", _OCSP_CLAIM, function (src) {
|
|
1889
|
+
if (!Buffer.isBuffer(src) && typeof src !== "string") throw _cfg("lint/bad-input", "pki.lint.ocsp input must be a parsed OCSP response, a DER Buffer, or a PEM string");
|
|
1890
|
+
return ocspSchema.parseResponse(src);
|
|
1891
|
+
}, _cfg, "lint/bad-input",
|
|
1892
|
+
"pki.lint.ocsp input must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signed byte range, the signature and the fields that range encodes are separate properties of a parsed object, so a rebuilt response could have them describe different responses") };
|
|
1893
|
+
} else {
|
|
1894
|
+
throw _cfg("lint/bad-input", "pki.lint.ocsp input must be a parsed OCSP response, a DER Buffer, or a PEM string");
|
|
1895
|
+
}
|
|
1896
|
+
try { return { response: ocspSchema.parseResponse(der) }; }
|
|
1897
|
+
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 6960", message: "input is not a well-formed OCSP response", context: { code: e.code } } }; }
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* @primitive pki.lint.ocsp
|
|
1902
|
+
* @signature pki.lint.ocsp(pem | der | parsed, opts?) -> LintReport
|
|
1903
|
+
* @since 0.7.26
|
|
1904
|
+
* @status stable
|
|
1905
|
+
* @spec RFC 6960, RFC 5019, RFC 9654
|
|
1906
|
+
* @related pki.lint.certificate, pki.lint.crl, pki.schema.ocsp.parseResponse, pki.ocsp.sign, pki.ocsp.verify
|
|
1907
|
+
*
|
|
1908
|
+
* Lint an OCSP response against the RFC 6960 profile, and on request against the RFC 5019
|
|
1909
|
+
* lightweight profile. `input` is a PEM string (`OCSP RESPONSE`), a DER `Buffer`, or an
|
|
1910
|
+
* already-parsed `pki.schema.ocsp.parseResponse` object. It returns the same `LintReport` shape
|
|
1911
|
+
* `pki.lint.certificate` returns, and it never throws on the data path: bytes that are not a
|
|
1912
|
+
* well-formed response produce a single `fatal` finding `lint/unparseable` carrying the strict
|
|
1913
|
+
* parser's own code. A non-successful response carries no BasicOCSPResponse and draws no row.
|
|
1914
|
+
*
|
|
1915
|
+
* The `rfc6960` rows cover each SingleResponse's update times and their relation to producedAt,
|
|
1916
|
+
* an empty `certs` field, the extended revoked definition (criticality, value and placement) and
|
|
1917
|
+
* the fixed shape of a revoked answer for a non-issued certificate, the placement of the nonce,
|
|
1918
|
+
* archive cutoff, CRL references and the request-only extensions, an archive cutoff later than
|
|
1919
|
+
* producedAt, the criticality and value syntax of the CRL entry extensions a SingleResponse may
|
|
1920
|
+
* carry, an unrecognized critical extension, the revocation reasons unspecified and removeFromCRL,
|
|
1921
|
+
* and two SingleResponses answering for one CertID. The `rfc5019` rows (nextUpdate present, one
|
|
1922
|
+
* SingleResponse, no responseExtensions, a byKey ResponderID) run only when that profile is
|
|
1923
|
+
* selected, since nothing in a response says which profile its responder follows; in the default
|
|
1924
|
+
* run they count as not applicable.
|
|
1925
|
+
*
|
|
1926
|
+
* Shapes the strict parser refuses never reach a rule and arrive as `lint/unparseable` carrying the
|
|
1927
|
+
* parser's code: a version other than v1, an undefined revocation reason, an empty or repeated
|
|
1928
|
+
* extension, an empty `responses`, a nonce outside 1..128 octets, a malformed archive cutoff or
|
|
1929
|
+
* CRL reference, a responseStatus that disagrees with responseBytes, and a time not in the
|
|
1930
|
+
* `YYYYMMDDHHMMSSZ` form RFC 5019 requires. What needs the issuer or the signature (responder
|
|
1931
|
+
* authorization, the ResponderID matching the signing certificate, freshness against a clock)
|
|
1932
|
+
* is `pki.ocsp.verify`'s, and the certificates in `certs` are linted with `pki.lint.certificate`.
|
|
1933
|
+
*
|
|
1934
|
+
* @opts profile `"rfc6960"` or `"rfc5019"` (default runs the rfc6960 rows and marks the rfc5019
|
|
1935
|
+
* rows not applicable). A certificate or CRL profile name is refused rather than
|
|
1936
|
+
* run against a response.
|
|
1937
|
+
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and `worst`
|
|
1938
|
+
* always reflect the complete, unfiltered result.
|
|
1939
|
+
* @example
|
|
1940
|
+
* var ca = await pki.key.generate("Ed25519");
|
|
1941
|
+
* var caKey = await pki.key.export(ca.privateKey);
|
|
1942
|
+
* var caDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(ca.publicKey),
|
|
1943
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
1944
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } }, { key: caKey });
|
|
1945
|
+
* var leaf = await pki.key.generate("Ed25519");
|
|
1946
|
+
* var leafDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(leaf.publicKey),
|
|
1947
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
1948
|
+
* { cert: caDer, key: caKey });
|
|
1949
|
+
* var der = await pki.ocsp.sign({ responderID: "byKey", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
|
|
1950
|
+
* { cert: caDer, key: caKey });
|
|
1951
|
+
* var report = pki.lint.ocsp(der);
|
|
1952
|
+
* report.findings.length; // -> 0
|
|
1953
|
+
* pki.lint.ocsp(der, { profile: "rfc5019" }).worst; // -> null
|
|
1954
|
+
*/
|
|
1955
|
+
function ocsp(input, opts) {
|
|
1956
|
+
opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
|
|
1957
|
+
guard.identifier.assertKnownKeys(opts, _OCSP_OPTS, _cfg, "lint/bad-input",
|
|
1958
|
+
"pki.lint.ocsp has an unknown option: ");
|
|
1959
|
+
if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
|
|
1960
|
+
throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
|
|
1961
|
+
}
|
|
1962
|
+
var profile = opts.profile;
|
|
1963
|
+
var rules_;
|
|
1964
|
+
if (profile == null || profile === "all" || profile === "default") rules_ = ALL_OCSP_RULES;
|
|
1965
|
+
else if (OCSP_PROFILES[profile]) rules_ = OCSP_PROFILES[profile];
|
|
1966
|
+
else if (PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a certificate profile; lint a certificate with pki.lint.certificate");
|
|
1967
|
+
else if (CRL_PROFILES[profile]) throw _cfg("lint/unknown-profile", "\"" + profile + "\" is a CRL profile; lint a CRL with pki.lint.crl");
|
|
1968
|
+
else throw _cfg("lint/unknown-profile", "unknown OCSP lint profile \"" + profile + "\" (known: " + Object.keys(OCSP_PROFILES).join(", ") + ")");
|
|
1969
|
+
|
|
1970
|
+
var ingested = _ingestOcsp(input);
|
|
1971
|
+
var report;
|
|
1972
|
+
if (ingested.fatal) {
|
|
1973
|
+
report = { findings: [ingested.fatal], ran: [],
|
|
1974
|
+
counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
|
|
1975
|
+
} else if (!ingested.response.basicResponse) {
|
|
1976
|
+
/** @internal A non-successful response carries no BasicOCSPResponse, so every row is not
|
|
1977
|
+
* applicable to it: the profile grades responses, and an error status is not one. */
|
|
1978
|
+
report = { findings: [], ran: [],
|
|
1979
|
+
counts: Object.assign(Object.create(null), { fatal: 0, error: 0, warn: 0, notice: 0, pass: 0, na: rules_.length, ne: 0 }) };
|
|
1980
|
+
} else {
|
|
1981
|
+
report = _runLints(rules_, ingested.response, { profile: profile, decode: function () { return null; }, raw: function () { return null; } });
|
|
1982
|
+
}
|
|
1983
|
+
report.worst = _worst(report.findings);
|
|
1984
|
+
report.findings = _applyThreshold(report, opts.severity);
|
|
1985
|
+
return report;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1465
1988
|
/**
|
|
1466
1989
|
* @primitive pki.lint.rules
|
|
1467
1990
|
* @signature pki.lint.rules(profile?) -> [{id, severity, source, citation}]
|
|
@@ -1477,8 +2000,8 @@ function crl(input, opts) {
|
|
|
1477
2000
|
*/
|
|
1478
2001
|
function rules(profile) {
|
|
1479
2002
|
var set = profile == null
|
|
1480
|
-
? ALL_RULES.concat(ALL_CRL_RULES)
|
|
1481
|
-
: (PROFILES[profile] || CRL_PROFILES[profile]
|
|
2003
|
+
? ALL_RULES.concat(ALL_CRL_RULES).concat(ALL_OCSP_RULES)
|
|
2004
|
+
: (PROFILES[profile] || CRL_PROFILES[profile] || OCSP_PROFILES[profile]
|
|
1482
2005
|
|| (function () { throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\""); })());
|
|
1483
2006
|
return set.map(function (r) { return { id: r.id, severity: r.severity, source: r.source, citation: r.citation }; });
|
|
1484
2007
|
}
|
|
@@ -1492,13 +2015,14 @@ function rules(profile) {
|
|
|
1492
2015
|
*
|
|
1493
2016
|
* List the known lint-profile names.
|
|
1494
2017
|
* @example
|
|
1495
|
-
* pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "rfc5280-crl"]
|
|
2018
|
+
* pki.lint.profiles(); // -> ["rfc5280", "rfc9881", "rfc9909", "rfc9935", "cabf-tls", "rfc5280-crl", "rfc6960", "rfc5019"]
|
|
1496
2019
|
*/
|
|
1497
|
-
function profiles() { return Object.keys(PROFILES).concat(Object.keys(CRL_PROFILES)); }
|
|
2020
|
+
function profiles() { return Object.keys(PROFILES).concat(Object.keys(CRL_PROFILES)).concat(Object.keys(OCSP_PROFILES)); }
|
|
1498
2021
|
|
|
1499
2022
|
module.exports = {
|
|
1500
2023
|
certificate: certificate,
|
|
1501
2024
|
crl: crl,
|
|
2025
|
+
ocsp: ocsp,
|
|
1502
2026
|
rules: rules,
|
|
1503
2027
|
profiles: profiles,
|
|
1504
2028
|
};
|