@blamejs/pki 0.5.3 → 0.5.5

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/lib/merkle.js CHANGED
@@ -188,8 +188,14 @@ function emptyRootHash() {
188
188
  * var lh = pki.merkle.leafHash(Buffer.from([0]));
189
189
  * pki.merkle.verifyInclusion({ leafIndex: 0, treeSize: 1, leafHash: lh, proof: [], rootHash: lh }); // -> true
190
190
  */
191
+ // Every field of this verb is REQUIRED, so a misspelling is the one input that reads as an omission
192
+ // rather than as a value -- and an omitted required field is caught, while a misspelled one leaves
193
+ // the caller believing they supplied a proof about a leaf they never named.
194
+ var _INCLUSION_KEYS = { leafIndex: 1, treeSize: 1, leafHash: 1, rootHash: 1, proof: 1 };
191
195
  function verifyInclusion(opts) {
192
196
  opts = opts || {};
197
+ guard.identifier.assertKnownKeys(opts, _INCLUSION_KEYS, function (c, m) { return new MerkleError(c, m); },
198
+ "merkle/bad-input", "unknown verifyInclusion option ");
193
199
  var leafIndex = _coerceCoord(opts.leafIndex, "leafIndex");
194
200
  var treeSize = _coerceCoord(opts.treeSize, "treeSize");
195
201
  if (treeSize === 0n) throw new MerkleError("merkle/empty-tree", "an empty tree has no leaves to include");
@@ -254,8 +260,11 @@ function verifyInclusion(opts) {
254
260
  * var r = pki.merkle.leafHash(Buffer.from([0]));
255
261
  * pki.merkle.verifyConsistency({ oldSize: 1, newSize: 1, oldRoot: r, newRoot: r, proof: [] }); // -> true
256
262
  */
263
+ var _CONSISTENCY_KEYS = { oldSize: 1, newSize: 1, oldRoot: 1, newRoot: 1, proof: 1 };
257
264
  function verifyConsistency(opts) {
258
265
  opts = opts || {};
266
+ guard.identifier.assertKnownKeys(opts, _CONSISTENCY_KEYS, function (c, m) { return new MerkleError(c, m); },
267
+ "merkle/bad-input", "unknown verifyConsistency option ");
259
268
  var oldSize = _coerceCoord(opts.oldSize, "oldSize");
260
269
  var newSize = _coerceCoord(opts.newSize, "newSize");
261
270
  var oldRoot = _node32(opts.oldRoot, "oldRoot");
package/lib/ocsp.js CHANGED
@@ -57,14 +57,31 @@ var OID_EXTENDED_REVOKE = O("ocspExtendedRevoke");
57
57
 
58
58
  function _digest(wcHash, buf) { return subtle.digest(wcHash, buf).then(function (h) { return Buffer.from(h); }); }
59
59
  function _certOf(arg, what) {
60
- if (arg && arg.subjectPublicKeyInfo && arg.tbsBytes) return arg; // already parsed
61
- var der;
62
- if (Buffer.isBuffer(arg)) der = arg;
63
- else if (arg instanceof Uint8Array) der = Buffer.from(arg);
64
- else if (typeof arg === "string") { try { der = x509.pemDecode(arg); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
65
- else throw _err("ocsp/bad-input", (what || "a certificate") + " must be a parsed certificate, a DER Buffer, or a PEM string");
66
- try { return x509.parse(der); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " is not a well-formed X.509 certificate", e); }
60
+ var label = what || "a certificate";
61
+ // Re-derived from the bytes its parser read, like every other certificate door. A CertID is an
62
+ // IDENTITY -- the issuer's name and key hashed, plus the serial -- so a certificate that names one
63
+ // identity while carrying another's signed bytes would have this verb ask about, or answer for,
64
+ // a certificate nobody issued.
65
+ return guard.parsed.acceptDerived(arg, "certificate", function (bytes) {
66
+ var der = bytes;
67
+ if (typeof bytes === "string") {
68
+ try { der = x509.pemDecode(bytes); } catch (e) { throw _err("ocsp/bad-input", label + " PEM could not be decoded", e); }
69
+ }
70
+ try { return x509.parse(der); } catch (e) { throw _err("ocsp/bad-input", label + " is not a well-formed X.509 certificate", e); }
71
+ }, _err, "ocsp/bad-input", label);
72
+ }
73
+ // The response, always parsed from the bytes the caller handed over. See verify's own comment for
74
+ // why an object cannot be accepted here: the three parts of a signature check would come from three
75
+ // independently-chosen properties. The claim is detected on any of the parsed-response fields, so a
76
+ // caller who passes one is told what happened rather than getting a byte-parse fault about its type.
77
+ var _RESPONSE_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
78
+ function _responseFromBytes(response) {
79
+ return guard.parsed.fromTrustedSource(response, "ocspResponse", _RESPONSE_CLAIM, function (bytes) {
80
+ return ocspSchema.parseResponse(_toDer(bytes, "the OCSP response"));
81
+ }, _err, "ocsp/bad-input",
82
+ "the OCSP response must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signature, the algorithm that verifies it and the bytes it covers are separate properties of a parsed object, so a REBUILT response (Object.assign, spread, a JSON round-trip) could have the three describe different responses and is refused");
67
83
  }
84
+
68
85
  function _toDer(input, what) {
69
86
  if (Buffer.isBuffer(input)) return input;
70
87
  if (input instanceof Uint8Array) return Buffer.from(input);
@@ -456,13 +473,26 @@ function verify(response, opts) {
456
473
  if (opts.cert == null || opts.issuer == null) return Promise.reject(_err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
457
474
  var parsed, cert, issuerCert, time;
458
475
  try {
459
- parsed = (response && response.responseStatus) ? response : ocspSchema.parseResponse(_toDer(response, "the OCSP response"));
476
+ // The response is parsed from BYTES, always. A claimed-parsed response carries the signature,
477
+ // the algorithm that verifies it and the byte range it covers as three independent properties,
478
+ // so an object could pair one structure's tbsResponseDataBytes with another's signature -- and a
479
+ // signature the issuing CA made over a certificate it issued would verify as a ResponseData
480
+ // signature, returning status "good" for a certificate the responder never spoke about.
481
+ // Parsing here binds all three to one byte string. pki.schema.ocsp.parseResponse remains the
482
+ // parse-only route for a caller who wants the structure without a verdict.
483
+ parsed = _responseFromBytes(response);
460
484
  cert = _certOf(opts.cert, "the target certificate");
461
485
  issuerCert = _certOf(opts.issuer, "the issuer certificate");
462
486
  // The time drives the currency + responder-cert validity windows; an invalid Date fails closed
463
487
  // via _asDate (a NaN compares false against every bound, silently disabling both), never defaults.
464
488
  time = opts.time == null ? new Date() : _asDate(opts.time);
465
489
  } catch (e) { return Promise.reject(e); }
490
+ // The object parsed HERE goes to the verdict verb, not the caller's argument again. It carries the
491
+ // parser's record, so the verdict verb re-derives from the same recorded bytes -- one snapshot,
492
+ // read by both. Passing the caller's argument a second time would take a SECOND snapshot of it,
493
+ // and a shared-memory view can differ between the two: the nonce compared below would belong to
494
+ // one response and the signature verified to another, which is the split this whole mechanism
495
+ // exists to close.
466
496
  return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time, { historicalMode: opts.historicalMode === true }).then(function (verdict) {
467
497
  // A client that sent no nonce still gets the field, as null. Leaving it absent would make
468
498
  // "not requested" indistinguishable from "the field is not there yet" for a consumer reading
@@ -1127,13 +1127,35 @@ function validateCriticalExtensionStructure(cert) {
1127
1127
  * The value-carrying options (`time`, `maxPathCerts`, `maxPolicyNodes`, the
1128
1128
  * subtree seeds, `userInitialPolicySet`, `requiredEku`) are validated at the
1129
1129
  * entry point -- a mis-shaped value throws `path/bad-input` rather than
1130
- * silently not applying. Returns `{ valid, path,
1131
- * results, workingPublicKey, workingPublicKeyAlgorithm,
1130
+ * silently not applying. Returns `{ valid, revocationChecked, anchorConstraints,
1131
+ * path, results, workingPublicKey, workingPublicKeyAlgorithm,
1132
1132
  * workingPublicKeyParameters, validPolicyTree }` where `results[i].checks`
1133
1133
  * carries a per-check reason code (`path/*`) for every step. Pure and
1134
1134
  * re-entrant -- no input object is mutated. An empty path or a missing anchor
1135
1135
  * throws a typed `PathError`.
1136
1136
  *
1137
+ * `valid` alone cannot say whether revocation was ever established, so
1138
+ * `revocationChecked` answers separately, taking the WEAKEST outcome on the
1139
+ * path: `false` when no `revocationChecker` was supplied, `"determined"` when
1140
+ * every certificate got an explicit good or revoked answer, `"waived"` when
1141
+ * `softFail` turned an undetermined one into a pass, and `"undetermined"` when
1142
+ * one could not be answered at all and the path fails for it. The
1143
+ * per-certificate `revocation` check carries the `status` it was decided on and
1144
+ * marks a waiver, so "checked, good" is distinguishable from "could not check,
1145
+ * and you waived it" -- which a stored verdict is re-read to settle. A checker
1146
+ * that THROWS is a fault in the checker rather than a status it reported, so it
1147
+ * fails the path as `path/revocation-checker-error` carrying the fault whatever
1148
+ * `softFail` says -- `softFail` opts into an undetermined ANSWER, and the
1149
+ * built-in checkers report one as `{ status: "unknown" }` rather than throwing.
1150
+ *
1151
+ * `anchorConstraints` reports what the anchor's own trust metadata decided:
1152
+ * the `checkedPurpose` it was judged under, and whether the `distrustAfter`
1153
+ * date and the `purposes` delegator map each applied. That metadata is keyed BY
1154
+ * key purpose, so an anchor carrying it while `opts.checkPurpose` is absent is a
1155
+ * configuration fault (`path/bad-input`) rather than a constraint that silently
1156
+ * does nothing -- a root distrusted years ago must not quietly validate a
1157
+ * current leaf.
1158
+ *
1137
1159
  * @example
1138
1160
  * var pair = await pki.key.generate("Ed25519");
1139
1161
  * var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
@@ -1156,7 +1178,10 @@ async function validate(path, opts) {
1156
1178
  // crypto amplification from an oversized path). Entry-point tier: throw.
1157
1179
  var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
1158
1180
  if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
1159
- var certs = path.map(function (c) { return (c && c.tbsBytes) ? c : x509.parse(c); });
1181
+ // Through the same door `build` uses, and for the same reason: every certificate this walk decides
1182
+ // over is re-derived from the bytes its parser read (see coerceCert), so a rebuilt object cannot
1183
+ // present a genuine signed range alongside substituted fields.
1184
+ var certs = path.map(function (c, ci) { return coerceCert(c, "validate: path[" + ci + "]"); });
1160
1185
  var n = certs.length;
1161
1186
  if (n < 1) throw E("path/empty-path", "validate: the certification path is empty");
1162
1187
  if (!opts.trustAnchor) throw E("path/bad-input", "validate: a trustAnchor is required");
@@ -1204,6 +1229,22 @@ async function validate(path, opts) {
1204
1229
  // then fails the path closed instead of silently skipping the step.
1205
1230
  var requireRevocation = opts.requireRevocation === true;
1206
1231
  var failed = false;
1232
+ // Whether the revocation rule RAN, and whether any certificate's answer had to be waived. The
1233
+ // verdict reports the two separately because they are different claims: a caller re-reading a
1234
+ // stored result needs to tell "every certificate was determined not revoked" from "the step was
1235
+ // skipped" from "the step could not conclude and you had asked for that to pass".
1236
+ var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
1237
+ // Which of the anchor's purpose-scoped constraints actually decided anything, reported so a
1238
+ // verdict can be re-read to tell an anchor that was judged from one that carried nothing to judge.
1239
+ var anchorDistrustApplied = false, anchorPurposeApplied = false;
1240
+ // A trust anchor carrying purpose-scoped metadata, validated with no purpose to select by, is a
1241
+ // configuration fault. The constraint is KEYED by purpose -- there is no way to apply
1242
+ // `distrustAfter.serverAuth` without being told the validation is about serverAuth -- so without
1243
+ // one the caller's stated intent would be discarded silently, and a root distrusted years ago
1244
+ // would validate a current leaf. An anchor carrying no such metadata is unaffected.
1245
+ if (!checkPurpose && opts.trustAnchor && _hasPurposeScopedMetadata(opts.trustAnchor)) {
1246
+ 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");
1247
+ }
1207
1248
 
1208
1249
  for (var idx = 0; idx < n; idx++) {
1209
1250
  var i = idx + 1;
@@ -1274,16 +1315,48 @@ async function validate(path, opts) {
1274
1315
  // 6.1.3(a)(3) revocation.
1275
1316
  if (revocationChecker) {
1276
1317
  var issuerCert = idx > 0 ? certs[idx - 1] : null; // the anchor issues cert[1]
1277
- var rv;
1318
+ var rv, rvError = null;
1319
+ // A checker that THROWS -- or whose promise rejects -- is not a checker reporting "unknown".
1320
+ // Laundering the two together made a broken checker indistinguishable from a working one that
1321
+ // could not reach the responder, and under softFail both became a pass. The fault is carried
1322
+ // onto the check so an operator can tell their own bug from a network condition, and it fails
1323
+ // the path whatever softFail says (see the branch below).
1278
1324
  try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true }); }
1279
- catch (_e) { rv = { status: "unknown" }; }
1325
+ catch (e) { rv = { status: "error" }; rvError = e; }
1280
1326
  // ONLY an explicit "good" is a determined non-revocation; "revoked" fails;
1281
1327
  // every other value ("unknown", an OCSP tryLater/unauthorized, a typo, a
1282
1328
  // missing status) is undetermined and fails closed unless softFail.
1283
- if (rv && rv.status === "good") { checks.push({ name: "revocation", ok: true }); }
1284
- else if (rv && rv.status === "revoked") { checks.push({ name: "revocation", ok: false, code: "path/revoked" }); failed = true; }
1285
- else if (softFail) { checks.push({ name: "revocation", ok: true }); }
1286
- else { checks.push({ name: "revocation", ok: false, code: "path/revocation-undetermined" }); failed = true; }
1329
+ //
1330
+ // The two `ok: true` outcomes are NOT the same claim and no longer the same object. "checked,
1331
+ // and it said good" and "could not check, and you waived it" read identically from a bare
1332
+ // boolean, which is the whole reason a stored verdict cannot be re-read to answer whether
1333
+ // revocation was ever established. Each entry now names the status it was decided on, and a
1334
+ // waiver marks itself.
1335
+ var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
1336
+ if (rv && rv.status === "good") { checks.push({ name: "revocation", ok: true, status: "good" }); }
1337
+ else if (rv && rv.status === "revoked") { checks.push({ name: "revocation", ok: false, status: "revoked", code: "path/revoked" }); failed = true; }
1338
+ else if (rvError) {
1339
+ // A checker that threw is a FAULT in the checker, not a status it could not reach, and
1340
+ // softFail is the caller opting into an undetermined ANSWER. The built-in CRL and OCSP
1341
+ // checkers return `{status:"unknown"}` for every unreachable or unverifiable condition and
1342
+ // never throw, so a throw here is the caller's own bug -- waiving it would pass the
1343
+ // certificate with no revocation result at all, which is the outcome softFail is asked for
1344
+ // and this is not.
1345
+ checks.push({ name: "revocation", ok: false, status: "error", code: "path/revocation-checker-error", error: rvError });
1346
+ revocationUndetermined = true;
1347
+ failed = true;
1348
+ }
1349
+ else if (softFail) {
1350
+ // No `error` slot here or below: a fault took the branch above, so anything reaching these
1351
+ // two is a status the checker actually reported.
1352
+ checks.push({ name: "revocation", ok: true, status: rvStatus, waived: true });
1353
+ revocationWaived = true;
1354
+ } else {
1355
+ checks.push({ name: "revocation", ok: false, status: rvStatus, code: "path/revocation-undetermined" });
1356
+ revocationUndetermined = true;
1357
+ failed = true;
1358
+ }
1359
+ revocationRan = true;
1287
1360
  } else if (requireRevocation) {
1288
1361
  // No checker was supplied but the caller demands a revocation determination:
1289
1362
  // the step cannot be performed, so fail closed (never silently skip).
@@ -1357,6 +1430,7 @@ async function validate(path, opts) {
1357
1430
  // before the comparison; an absent (undefined/null) date is no restriction.
1358
1431
  var distrustDate = assertAnchorConstraints(ta, checkPurpose);
1359
1432
  if (distrustDate != null) {
1433
+ anchorDistrustApplied = true;
1360
1434
  // STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
1361
1435
  // (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
1362
1436
  // <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
@@ -1365,8 +1439,11 @@ async function validate(path, opts) {
1365
1439
  checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
1366
1440
  }
1367
1441
  }
1368
- if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
1369
- checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1442
+ if (checkPurpose && ta.purposes) {
1443
+ anchorPurposeApplied = true;
1444
+ if (ta.purposes[checkPurpose] !== true) {
1445
+ checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1446
+ }
1370
1447
  }
1371
1448
  updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
1372
1449
  }
@@ -1404,6 +1481,20 @@ async function validate(path, opts) {
1404
1481
 
1405
1482
  return {
1406
1483
  valid: !failed,
1484
+ // What the revocation rule and the anchor's own trust metadata actually decided. `valid` alone
1485
+ // cannot answer either, and both are questions a stored verdict is re-read to settle: was this
1486
+ // certificate ever established as un-revoked, and was the anchor's distrust date consulted.
1487
+ // The WEAKEST outcome on the path, not the fact that a checker ran: a certificate nobody could
1488
+ // answer for leaves revocation unestablished however many others answered, and deriving the word
1489
+ // from "a checker ran" put that run on the same value as one that established every answer.
1490
+ revocationChecked: !revocationRan ? false
1491
+ : revocationUndetermined ? "undetermined"
1492
+ : revocationWaived ? "waived" : "determined",
1493
+ anchorConstraints: {
1494
+ checkedPurpose: checkPurpose || null,
1495
+ distrustAfterApplied: anchorDistrustApplied,
1496
+ purposeTrustApplied: anchorPurposeApplied,
1497
+ },
1407
1498
  path: certs,
1408
1499
  results: state.results,
1409
1500
  workingPublicKey: state.workingPublicKey,
@@ -1446,24 +1537,10 @@ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
1446
1537
  var OID_CRL_NUMBER = oid.byName("cRLNumber");
1447
1538
  var OID_FRESHEST_CRL = oid.byName("freshestCRL");
1448
1539
 
1449
- // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
1450
- // onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
1451
- // onlySomeReasons [3] ReasonFlags OPTIONAL, indirectCRL [4] DEFAULT FALSE,
1452
- // onlyContainsAttributeCerts [5] DEFAULT FALSE } (RFC 5280 sec. 5.2.5). Declared
1453
- // through the engine so the trailing-field grammar (strictly-ascending tags, each
1454
- // at most once) and the DER BOOLEAN value rules are the shared enforcement, not a
1455
- // hand-walk; a present DEFAULT-FALSE flag encoding FALSE is the omitted default
1456
- // (X.690 sec. 11.5) and rejects at the leaf-value level below.
1457
- var IDP_SCHEMA = schema.seq([
1458
- schema.trailing([
1459
- { tag: 0, name: "distributionPoint", schema: schema.any() },
1460
- { tag: 1, name: "onlyContainsUserCerts", schema: schema.implicitBoolean(1) },
1461
- { tag: 2, name: "onlyContainsCACerts", schema: schema.implicitBoolean(2) },
1462
- { tag: 3, name: "onlySomeReasons", schema: schema.implicitBitString(3) },
1463
- { tag: 4, name: "indirectCRL", schema: schema.implicitBoolean(4) },
1464
- { tag: 5, name: "onlyContainsAttributeCerts", schema: schema.implicitBoolean(5) },
1465
- ], { minTag: 0, maxTag: 5, unexpectedCode: "path/bad-idp", orderCode: "path/bad-idp" }),
1466
- ], { assert: "sequence", code: "path/bad-idp", what: "IssuingDistributionPoint" });
1540
+ // The RFC 5280 sec. 5.2.5 IssuingDistributionPoint grammar, shared with the CRL
1541
+ // verbs (pkix.issuingDistributionPoint) so the scope this validator reads and the
1542
+ // scope pki.crl.isRevoked refuses to answer past are read by the same rules.
1543
+ var IDP_SCHEMA = pkix.issuingDistributionPoint("path/bad-idp");
1467
1544
 
1468
1545
  // RFC 5280 sec. 6.3.2(a): the legal members of reasons_mask are exactly the eight
1469
1546
  // named ReasonFlags bits, 1..8 (`unused` bit 0 is not a reason, and `unspecified`
@@ -1782,7 +1859,16 @@ function selectDelta(candidates) {
1782
1859
  * typeof checker.check; // "function"
1783
1860
  */
1784
1861
  function crlChecker(crls, opts) {
1785
- var parsed = (crls || []).map(function (c) { return (c && c.tbsBytes) ? c : crl.parse(c); });
1862
+ var parsed = (crls || []).map(function (c, ci) {
1863
+ // Re-derived from the bytes the parser read, exactly as a certificate is. A CRL's answer is a
1864
+ // verdict, and its signature covers a byte range while the revocation list and the scope
1865
+ // extensions are separate properties of the parsed object: keep a correctly signed CRL's
1866
+ // `tbsBytes` and signature, empty `revokedCertificates`, and the signature still verifies while
1867
+ // a revoked certificate reports good. The scope fields fail the same way -- an emptied
1868
+ // `crlExtensions` turns a scope-restricted CRL into one that answers for everything.
1869
+ return guard.parsed.acceptDerived(c, "crl", crl.parse, E, "path/bad-input",
1870
+ "crlChecker: crls[" + ci + "]");
1871
+ });
1786
1872
  // RFC 5280 sec. 6.3.1(b): use-deltas is an INPUT to the algorithm. Default ON --
1787
1873
  // a caller holding a delta wants it used -- and turning it off never makes a
1788
1874
  // verdict weaker, only less determined.
@@ -2222,7 +2308,10 @@ cmsVerify.setEngine({ build: build, validate: validate, toAnchor: toAnchor,
2222
2308
  * typeof checker.check; // "function"
2223
2309
  */
2224
2310
  function ocspChecker(responses) {
2225
- var parsed = (responses || []).map(function (r) { return (r && r.responseStatus) ? r : ocsp.parseResponse(r); });
2311
+ // The same door verifyOcspResponse uses. This checker's responses reach the identical three-part
2312
+ // signature check, so accepting a parsed object here on a truthy responseStatus would leave the
2313
+ // door shut on one of the two ways into that check and open on the other.
2314
+ var parsed = (responses || []).map(function (r) { return _ocspFromBytes(r); });
2226
2315
  return {
2227
2316
  check: async function (cert, issuer, ctx) {
2228
2317
  var time = ctx.time;
@@ -2272,14 +2361,14 @@ function ocspChecker(responses) {
2272
2361
 
2273
2362
  /**
2274
2363
  * @primitive pki.path.verifyOcspResponse
2275
- * @signature pki.path.verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>
2364
+ * @signature pki.path.verifyOcspResponse(response, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>
2276
2365
  * @since 0.2.22
2277
2366
  * @status stable
2278
2367
  * @spec RFC 6960
2279
2368
  * @related pki.ocsp.verify, pki.path.ocspChecker
2280
2369
  *
2281
- * Verify a single already-parsed OCSP response for one certificate against its
2282
- * already-parsed issuer certificate at `time` -- the lower-level primitive
2370
+ * Verify a single OCSP response for one certificate
2371
+ * against its already-parsed issuer certificate at `time` -- the lower-level primitive
2283
2372
  * `pki.ocsp.verify` composes after parsing its inputs (most callers want that
2284
2373
  * ergonomic entry, which also handles DER/PEM decoding and request-nonce
2285
2374
  * matching). It runs the EXACT SAME gates the path validator's `ocspChecker`
@@ -2300,6 +2389,16 @@ function ocspChecker(responses) {
2300
2389
  * `time` either way. `time` must be a valid `Date`. A malformed response's parse
2301
2390
  * fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
2302
2391
  *
2392
+ * The response is its DER bytes, a PEM string, or an unmodified
2393
+ * `pki.schema.ocsp.parseResponse` result. A REBUILT parsed response is refused. A
2394
+ * signature check has three parts -- the signature, the algorithm that verifies it,
2395
+ * and the bytes it covers -- and on a parsed object all three are separate properties:
2396
+ * a genuine CA signature over a certificate that CA issued, relabelled, verifies as a
2397
+ * ResponseData signature for a response that never existed. The parser marks what it
2398
+ * returns, so those three are known to have been derived together from one byte
2399
+ * string; `Object.assign`, spread and a JSON round-trip all drop the mark, which is
2400
+ * exactly how such an object is assembled.
2401
+ *
2303
2402
  * @example
2304
2403
  * var ca = await pki.key.generate("Ed25519");
2305
2404
  * var caKey = await pki.key.export(ca.privateKey);
@@ -2315,11 +2414,49 @@ function ocspChecker(responses) {
2315
2414
  * { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
2316
2415
  * { cert: caDer, key: caKey });
2317
2416
  * var cert = pki.schema.x509.parse(leafDer), issuerCert = pki.schema.x509.parse(caDer);
2318
- * var resp = pki.schema.ocsp.parseResponse(der);
2319
- * var v = await pki.path.verifyOcspResponse(resp, cert, issuerCert, new Date());
2417
+ * var v = await pki.path.verifyOcspResponse(der, cert, issuerCert, new Date());
2320
2418
  * v.status; // "good" | "revoked" | "unknown"
2321
2419
  */
2322
- function verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts) {
2420
+ function verifyOcspResponse(response, cert, issuerCert, time, opts) {
2421
+ var parsedResponse, subject, issuer;
2422
+ try {
2423
+ parsedResponse = _ocspFromBytes(response);
2424
+ // The two certificates go through the same door as the response, for the same reason: this
2425
+ // verdict is about a certificate's IDENTITY -- its serial and its issuer's name and key are
2426
+ // what the CertID is matched against, and the issuer's key is what authorizes a delegated
2427
+ // responder. A caller-assembled certificate could name one identity while carrying another's
2428
+ // signed bytes, so both are re-derived from the bytes their parser read.
2429
+ subject = coerceCert(cert, "the certificate");
2430
+ issuer = coerceCert(issuerCert, "the issuer certificate");
2431
+ } catch (e) { return Promise.reject(e); }
2432
+ return _verifyOcspParsed(parsedResponse, subject, issuer, time, opts);
2433
+ }
2434
+
2435
+ // The response, always parsed from the bytes the caller handed over.
2436
+ //
2437
+ // A signature check has three parts -- the signature, the algorithm that verifies it, and the byte
2438
+ // range it covers -- and on a claimed-parsed response all three are separate properties of one
2439
+ // caller-supplied object. Pair a real CA's signature over a certificate IT issued with that
2440
+ // certificate's own tbsBytes and algorithm, label them `signature` / `tbsResponseDataBytes` /
2441
+ // `signatureAlgorithm`, and the check verifies: a genuine signature, over the bytes it was made
2442
+ // over, under the right key -- for a structure the responder never produced. Parsing binds the three
2443
+ // to one byte string, which is the only thing that makes the verdict about a response at all.
2444
+ var _OCSP_CLAIM = ["responseStatus", "basicResponse", "tbsResponseDataBytes"];
2445
+ function _ocspFromBytes(response) {
2446
+ return guard.parsed.fromTrustedSource(response, "ocspResponse", _OCSP_CLAIM, function (bytes) {
2447
+ if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array) && typeof bytes !== "string") {
2448
+ throw E("path/bad-input", "verifyOcspResponse: the response must be an OCSP response DER Buffer, a PEM string, or a pki.schema.ocsp.parseResponse result");
2449
+ }
2450
+ return ocsp.parseResponse(bytes);
2451
+ }, E, "path/bad-input",
2452
+ "verifyOcspResponse: the response must be its DER bytes, a PEM string, or an unmodified pki.schema.ocsp.parseResponse result: the signature, the algorithm that verifies it and the bytes it covers are separate properties of a parsed object, so a REBUILT response (Object.assign, spread, a JSON round-trip) could have the three describe different responses and is refused");
2453
+ }
2454
+
2455
+ // The verification over a response this module has ALREADY derived from bytes -- reached only
2456
+ // through the door above and through ocspChecker, which parses the responder's own reply. Not
2457
+ // exported: this module's export object IS pki.path, so anything on it is public surface and frozen
2458
+ // by the API snapshot, whatever a comment beside it claims.
2459
+ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
2323
2460
  opts = opts || {};
2324
2461
  // The currency + responder-cert validity windows compare against `time`; a missing or invalid
2325
2462
  // check date must fail closed (a NaN compares false against every bound), never silently pass.
@@ -2377,41 +2514,22 @@ function nameMatchSoft(rdnsA, rdnsB) {
2377
2514
  catch (_e) { return false; }
2378
2515
  }
2379
2516
 
2380
- // A parsed extension entry the search's findExt dereferences by .oid and, for
2381
- // the subjectAltName, by .value (a Buffer in the identity key).
2382
- function _isExtensionEntry(e) { return !!e && typeof e.oid === "string" && Buffer.isBuffer(e.value); }
2383
-
2384
- // The complete parsed-certificate shape build produces AND hands to validate --
2385
- // every top-level field this module dereferences (grep-verified), each with the
2386
- // type the code assumes. A claimed-parsed object satisfying this cannot throw a
2387
- // raw TypeError anywhere in the search or the validate hand-off.
2388
- function _isParsedCert(o) {
2389
- return Buffer.isBuffer(o.tbsBytes) &&
2390
- typeof o.serialNumberHex === "string" &&
2391
- !!o.signatureAlgorithm && typeof o.signatureAlgorithm.oid === "string" &&
2392
- !!o.signatureValue && Buffer.isBuffer(o.signatureValue.bytes) &&
2393
- !!o.validity && o.validity.notBefore instanceof Date && o.validity.notAfter instanceof Date &&
2394
- !!o.issuer && Array.isArray(o.issuer.rdns) &&
2395
- !!o.subject && Array.isArray(o.subject.rdns) && Buffer.isBuffer(o.subject.bytes) &&
2396
- !!o.subjectPublicKeyInfo && Buffer.isBuffer(o.subjectPublicKeyInfo.bytes) &&
2397
- !!o.subjectPublicKeyInfo.algorithm && typeof o.subjectPublicKeyInfo.algorithm.oid === "string" &&
2398
- !!o.subjectPublicKeyInfo.publicKey && Buffer.isBuffer(o.subjectPublicKeyInfo.publicKey.bytes) &&
2399
- typeof o.subjectPublicKeyInfo.publicKey.unusedBits === "number" &&
2400
- Array.isArray(o.extensions) && o.extensions.every(_isExtensionEntry);
2401
- }
2402
-
2403
- function coerceCert(input) {
2404
- // An already-parsed certificate is passed through; a DER Buffer / PEM string is
2405
- // parsed (and its typed error normalized by the caller). A claimed-parsed object
2406
- // (a truthy tbsBytes) must carry the COMPLETE parsed-certificate shape build and
2407
- // the validate hand-off dereference -- a bare { tbsBytes } object, or any partial
2408
- // shape, fails closed here as a typed PathError rather than a raw TypeError deeper
2409
- // in the walk or inside validate.
2410
- if (input && typeof input === "object" && !Buffer.isBuffer(input) && input.tbsBytes !== undefined) {
2411
- if (!_isParsedCert(input)) throw E("path/bad-input", "build: an input has tbsBytes but is not a well-formed parsed certificate");
2412
- return input;
2413
- }
2414
- return x509.parse(input);
2517
+ // A certificate reaching a VERDICT is re-derived from the bytes the parser read, never trusted as
2518
+ // the object it arrives as. Completeness -- every field present with the right type -- is not enough
2519
+ // here, because a certificate's meaning is one signature over one byte range while a parsed
2520
+ // certificate presents that range, the signature, and the fields the range encodes as separate
2521
+ // properties. Keep a real CA certificate's `tbsBytes` and signature and substitute only its
2522
+ // `subjectPublicKeyInfo`, and every completeness rule passes, this walk verifies the ORIGINAL signed
2523
+ // range, and then uses the substituted key to check the next certificate -- a forged chain built out
2524
+ // of a genuine certificate. Emptying `extensions` is the same move against basicConstraints,
2525
+ // keyUsage, the name constraints and the unknown-critical rule.
2526
+ //
2527
+ // So the door takes the parser's record instead: a certificate from pki.schema.x509.parse re-parses
2528
+ // from the bytes it was read from, and anything done to that object since is discarded. One a caller
2529
+ // assembled has no record and is refused rather than silently believed.
2530
+ function coerceCert(input, label) {
2531
+ return guard.parsed.acceptDerived(input, "certificate", x509.parse, E, "path/bad-input",
2532
+ label || "a certificate");
2415
2533
  }
2416
2534
 
2417
2535
  // A trust-store entry is either a ready anchor tuple { name, publicKey,
@@ -2437,6 +2555,17 @@ function coerceCert(input) {
2437
2555
  // exposed for the same reason: a caller that may never reach the walk -- pki.cms.verify when no
2438
2556
  // signer verified -- has to be able to reject a malformed anchor at ITS entry point, through this
2439
2557
  // same definition, so configuration validity never depends on the message.
2558
+ // Does this anchor carry trust metadata that only a named key purpose can unlock? A non-empty
2559
+ // `distrustAfter` or `purposes` map is such metadata -- both are indexed BY purpose, so both are
2560
+ // inert without one. An EMPTY map states no constraint and is not a reason to refuse.
2561
+ function _hasPurposeScopedMetadata(ta) {
2562
+ if (!ta || typeof ta !== "object") return false;
2563
+ return ["distrustAfter", "purposes"].some(function (k) {
2564
+ var m = ta[k];
2565
+ return !!m && typeof m === "object" && Object.keys(m).length > 0;
2566
+ });
2567
+ }
2568
+
2440
2569
  function assertAnchorConstraints(ta, checkPurpose) {
2441
2570
  var d = (checkPurpose && ta && ta.distrustAfter) ? ta.distrustAfter[checkPurpose] : null;
2442
2571
  if (d == null) return null;
@@ -296,6 +296,24 @@ function _buildBag(bag, opts, depth) {
296
296
  }
297
297
 
298
298
  // A pre-encoded DER value (one well-formed TLV, no trailing bytes) supplied verbatim (secretValue).
299
+ // The store, always parsed from the BYTES the caller handed over.
300
+ //
301
+ // Both verbs that use this decide integrity, and integrity is the one operation that must not read
302
+ // its inputs from two independently-chosen properties. Accepting a claimed-parsed store let
303
+ // `macedBytes` -- the range the MAC is verified over -- and `safeBags` / `encryptedSafes` -- the
304
+ // content returned as verified -- come from different sources: verify store A's MAC, hand back store
305
+ // B's bags. Parsing here binds them, because both are then derived from one byte string.
306
+ //
307
+ // `pki.schema.pkcs12.parse` remains the parse-only route for a caller who wants the structure
308
+ // without an integrity decision.
309
+ var _STORE_CLAIM = ["integrityMode", "mac", "macedBytes"];
310
+ function _storeFromBytes(pfx) {
311
+ return guard.parsed.fromTrustedSource(pfx, "pkcs12Store", _STORE_CLAIM, function (bytes) {
312
+ return schemaPkcs12.parse(_coerceDer(bytes, "pfx"));
313
+ }, _err, "pkcs12/bad-input",
314
+ "pfx must be the store's DER bytes, a PEM string, or an unmodified pki.schema.pkcs12.parse result: the MAC is verified over a byte range carried on the object and the bags returned as verified are a separate property of it, so a REBUILT store (Object.assign, spread, a JSON round-trip) could have the two describe different stores and is refused");
315
+ }
316
+
299
317
  function _reqDer(input, label) {
300
318
  if (input == null) throw _err("pkcs12/bad-input", label + " is required");
301
319
  var der = _bytes(input, label);
@@ -522,8 +540,10 @@ async function build(spec, opts) {
522
540
  * @defends pkcs12-mac-forgery (CWE-347)
523
541
  * @related pki.pkcs12.build, pki.schema.pkcs12.parse
524
542
  *
525
- * Verify a password-integrity PKCS#12 store's MAC. `pfx` is a `pki.schema.pkcs12.parse` result, a DER
526
- * `Buffer`, or a PEM string. The password is BMPString+NULL encoded (RFC 7292 App. B.1), the MAC is
543
+ * Verify a password-integrity PKCS#12 store's MAC. `pfx` is the store's DER `Buffer`, a PEM string, or an
544
+ * unmodified `pki.schema.pkcs12.parse` result. A REBUILT parsed store is refused: the MAC is verified over
545
+ * a byte range the object carries, and the parser's mark is what says that range and the store it describes
546
+ * came from one place. `Object.assign`, spread and a JSON round-trip drop the mark. The password is BMPString+NULL encoded (RFC 7292 App. B.1), the MAC is
527
547
  * recomputed over the store's exact AuthenticatedSafe byte range (`macedBytes`) using the store's own MAC
528
548
  * parameters -- the classic Appendix B (ID=3) HMAC or the RFC 9579 PBMAC1 -- and constant-time-compared to
529
549
  * the stored MAC value. Returns `true` / `false` for the password match; throws `Pkcs12Error` on a MAC-less
@@ -538,8 +558,14 @@ async function build(spec, opts) {
538
558
  * var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
539
559
  */
540
560
  async function verifyMac(pfx, password, opts) {
561
+ return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
562
+ }
563
+
564
+ // The MAC computation over a store this module has ALREADY parsed from bytes. Separate from the
565
+ // public verb so `open` can verify the store it just parsed without going back through the door --
566
+ // the door's job is to establish that the store came from the caller's bytes, and by here it has.
567
+ async function _verifyMacOfStore(m, password, opts) {
541
568
  opts = opts || {};
542
- var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
543
569
  if (m.integrityMode !== "password" || !m.mac) throw _err("pkcs12/bad-input", "the store carries no password MAC (integrityMode " + m.integrityMode + ")");
544
570
  var expected = m.mac.macValue;
545
571
  var computed;
@@ -600,8 +626,9 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
600
626
  * (public-key privacy) safe with `opts.recipientKey` (RFC 7292 sec. 3.1, via `pki.cms.decrypt`) -- returning a
601
627
  * structured bundle `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` -- each private key as PKCS#8
602
628
  * `PrivateKeyInfo` DER (re-validated), each certificate / CRL / secret as raw DER, all carrying their
603
- * `friendlyName` / `localKeyId` for pairing. `pfx` is a DER `Buffer`, PEM string, or a
604
- * `pki.schema.pkcs12.parse` result.
629
+ * `friendlyName` / `localKeyId` for pairing. `pfx` is the store's DER `Buffer`, a PEM string, or an
630
+ * unmodified `pki.schema.pkcs12.parse` result; a REBUILT parsed store is refused, since the bytes whose
631
+ * integrity is checked and the bags returned as checked are separate properties of it.
605
632
  *
606
633
  * A MAC-less store is refused (`pkcs12/no-integrity`) unless `opts.allowUnauthenticated` is set. A public-key
607
634
  * integrity store is verified through `pki.cms.verify` before any bag is trusted; a signature failure is
@@ -635,7 +662,7 @@ async function open(pfx, password, opts) {
635
662
  if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
636
663
  throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
637
664
  }
638
- var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
665
+ var m = _storeFromBytes(pfx);
639
666
  var macVerified = false;
640
667
  var signers = null;
641
668
  if (m.integrityMode === "public-key") {
@@ -649,7 +676,7 @@ async function open(pfx, password, opts) {
649
676
  if (!res.valid) throw _err("pkcs12/signature-invalid", "the PKCS#12 SignedData signature did not verify (an untrusted or tampered store)");
650
677
  signers = res.signers;
651
678
  } else if (m.integrityMode === "password") {
652
- macVerified = await verifyMac(m, password, opts);
679
+ macVerified = await _verifyMacOfStore(m, password, opts);
653
680
  if (!macVerified) throw _err("pkcs12/mac-mismatch", "the PKCS#12 MAC did not verify (wrong password or a tampered store)");
654
681
  } else if (!opts.allowUnauthenticated) {
655
682
  throw _err("pkcs12/no-integrity", "the store carries no integrity MAC (integrityMode " + m.integrityMode + "); set opts.allowUnauthenticated to open it anyway");
package/lib/pki-build.js CHANGED
@@ -395,4 +395,15 @@ function tbsNameField(cert, which) {
395
395
  return tbs.children[(hasVersion ? 1 : 0) + (which === "subject" ? 4 : 2)].bytes;
396
396
  }
397
397
 
398
- module.exports = { makeBuilder: makeBuilder, KU_BIT: KU_BIT, tbsNameField: tbsNameField };
398
+ // The serialNumber of a parsed X.509 certificate, read from the SIGNED bytes rather than off the
399
+ // object. Same tbs layout as above, and the same reason: where a producer binds an identity to an
400
+ // existing certificate, that identity is issuer AND serial together. Deriving one from the bytes
401
+ // and reading the other from the object lets the halves name different certificates -- a Holder
402
+ // whose issuer is genuine and whose serial is whatever the caller wrote.
403
+ function tbsSerialNumber(cert) {
404
+ var tbs = asn1.decode(cert.tbsBytes);
405
+ var hasVersion = tbs.children[0].tagClass === "context" && tbs.children[0].tagNumber === 0;
406
+ return asn1.read.integer(tbs.children[hasVersion ? 1 : 0]);
407
+ }
408
+
409
+ module.exports = { makeBuilder: makeBuilder, KU_BIT: KU_BIT, tbsNameField: tbsNameField, tbsSerialNumber: tbsSerialNumber };
package/lib/schema-crl.js CHANGED
@@ -217,7 +217,13 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
217
217
  * var crl = pki.schema.crl.parse(der);
218
218
  * crl.revokedCertificates[0].serialNumberHex; // -> "0a3f"
219
219
  */
220
- var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
220
+ // Recording, for the same reason the certificate parser is: a CRL is one signature over one byte
221
+ // range, and a parsed CRL presents that range and the revocation list it encodes as separate
222
+ // properties. Keep a correctly signed CRL's `tbsBytes` and signature and empty `revokedCertificates`
223
+ // and the signature still verifies while the revocation answer comes from the edited list -- a
224
+ // revoked certificate reported as good. The scope fields (`crlExtensions`, an IDP, a delta
225
+ // indicator) fail the same way. The verdict verbs re-parse from what is recorded here.
226
+ var parse = pkix.makeRecordingParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS }, "crl");
221
227
 
222
228
  /**
223
229
  * @primitive pki.schema.crl.pemDecode