@blamejs/pki 0.8.56 → 0.8.58

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/inspect.js CHANGED
@@ -1063,7 +1063,7 @@ function _looksParsedOcspResp(o) {
1063
1063
  var st = o.responseStatus;
1064
1064
  if (!st || typeof st !== "object" || typeof st.code !== "number" || typeof st.name !== "string") return false;
1065
1065
  var br = o.basicResponse;
1066
- /** @internal A response with no basic response is a status-only answer the report handles, but one that
1066
+ /** @internal A response with no basic response is an envelope-only answer the report handles, but one that
1067
1067
  * HAS a basic response has to carry the answers the report walks: a responderID and a responses list whose
1068
1068
  * entries name a CertID and a status. */
1069
1069
  if (br === null || br === undefined) return true;
package/lib/lint.js CHANGED
@@ -5087,6 +5087,51 @@ var RFC5652_CMS_RULES = [
5087
5087
  return (encap && encap.eContent) ? null : true;
5088
5088
  },
5089
5089
  },
5090
+ {
5091
+ id: "lint/rfc5652/other-revocation-format-not-read", severity: "notice", source: "rfc5652",
5092
+ citation: "RFC 5940 4",
5093
+ /** @internal The carriage is sec. 10.2.1's `other` arm, which is why the row sits in this
5094
+ * profile; the document that registers the formats is the citation. RFC 5940 sec. 2 registers
5095
+ * `id-ri-ocsp-response` for an OCSP response carried in the `crls` field and sec. 4 registers
5096
+ * `id-ri-scvp` for an SCVP request-response pair. The parse reads the first and leaves the second
5097
+ * as raw bytes, because acting on it needs an SCVP schema this build does not carry. An
5098
+ * unregistered format is opaque for the same reason. Neither is a parse fault: sec. 10.2.1's ANY
5099
+ * DEFINED BY is an extension point, so refusing an unknown format would refuse a future
5100
+ * registered one. The finding tells an operator that revocation information present in the
5101
+ * message was not read, which a reader counting on it needs to know. A notice: the message is
5102
+ * conformant and the entry may be irrelevant to the certificates it travels with.
5103
+ * An id-ri-ocsp-response entry whose OCSPResponse states a responseType this build does not
5104
+ * decode (RFC 6960 sec. 4.2.1) is reported for the same reason: the format was recognized, the
5105
+ * revocation information still was not read, and `responseType` in the context names why. */
5106
+ message: "a crls entry carries revocation information this build does not read, so it was not consulted",
5107
+ /** @internal Every content type that can carry a RevocationInfoChoice: SignedData's own `crls`
5108
+ * and the `originatorInfo.crls` that EnvelopedData, AuthenticatedData and AuthEnvelopedData
5109
+ * share. The buckets are read by presence rather than by content type, so one predicate admits
5110
+ * each carrier and a type that carries neither draws nothing. */
5111
+ appliesTo: function (cms) {
5112
+ return Array.isArray(cms.crls) || !!(cms.originatorInfo && Array.isArray(cms.originatorInfo.crls));
5113
+ },
5114
+ check: function (cms) {
5115
+ var out = [];
5116
+ var buckets = [{ where: "crls", list: cms.crls },
5117
+ { where: "originatorInfo.crls", list: cms.originatorInfo && cms.originatorInfo.crls }];
5118
+ buckets.forEach(function (bucket) {
5119
+ if (!Array.isArray(bucket.list)) return;
5120
+ bucket.list.forEach(function (entry, index) {
5121
+ if (!entry || entry.tagClass !== "context") return;
5122
+ var resp = entry.ocspResponse;
5123
+ if (resp && resp.basicResponse) return;
5124
+ var rb = resp && resp.responseBytes;
5125
+ out.push({ context: { placement: bucket.where, index: index,
5126
+ otherRevInfoFormat: entry.otherRevInfoFormat,
5127
+ otherRevInfoFormatName: entry.otherRevInfoFormatName,
5128
+ responseType: (rb && rb.responseType) || null,
5129
+ responseTypeName: (rb && rb.responseTypeName) || null } });
5130
+ });
5131
+ });
5132
+ return out.length ? out : null;
5133
+ },
5134
+ },
5090
5135
  ];
5091
5136
 
5092
5137
  /** @internal RFC 8933 updates RFC 5652 rather than restating it, so its rows are their own profile:
@@ -7141,7 +7186,9 @@ function _ingestOcsp(input) {
7141
7186
  * already-parsed `pki.schema.ocsp.parseResponse` object. It returns the same `LintReport` shape
7142
7187
  * `pki.lint.certificate` returns, and it never throws on the data path: bytes that are not a
7143
7188
  * well-formed response produce a single `fatal` finding `lint/unparseable` carrying the strict
7144
- * parser's own code. A non-successful response carries no BasicOCSPResponse and draws no row.
7189
+ * parser's own code. A response with no BasicOCSPResponse to grade draws no row: a non-successful
7190
+ * status carries none, and neither does a response carried in a CMS `crls` entry whose
7191
+ * `responseType` this build does not decode.
7145
7192
  *
7146
7193
  * The `rfc6960` rows cover each SingleResponse's update times and their relation to producedAt,
7147
7194
  * an empty `certs` field, the extended revoked definition (criticality, value and placement) and
@@ -7203,8 +7250,9 @@ function ocsp(input, opts) {
7203
7250
  report = { findings: [ingested.fatal], ran: [],
7204
7251
  counts: Object.assign(Object.create(null), { fatal: 1, error: 0, warn: 0, notice: 0, pass: 0, na: 0, ne: 0 }) };
7205
7252
  } else if (!ingested.response.basicResponse) {
7206
- /** @internal A non-successful response carries no BasicOCSPResponse, so every row is not
7207
- * applicable to it: the profile grades responses, and an error status is not one. */
7253
+ /** @internal Every row is not applicable to a response with no BasicOCSPResponse: the profile
7254
+ * grades a response body, and neither an error status nor a responseType this build does not
7255
+ * decode produces one. */
7208
7256
  report = { findings: [], ran: [],
7209
7257
  counts: Object.assign(Object.create(null), { fatal: 0, error: 0, warn: 0, notice: 0, pass: 0, na: rules_.length, ne: 0 }) };
7210
7258
  } else {
package/lib/ocsp.js CHANGED
@@ -659,6 +659,13 @@ function buildErrorResponse(status) {
659
659
  * replayed would hand a soft-failing caller the very certificate the responder refused. A replayed
660
660
  * `revoked` is therefore reported as `revoked` with `nonceMatched: false`.
661
661
  *
662
+ * `response` is DER `Buffer` / `Uint8Array`, a PEM `OCSP RESPONSE` string, or an unmodified
663
+ * `pki.schema.ocsp.parseResponse` result, which includes the record a CMS `crls` entry surfaces in
664
+ * `ocspResponse`. From bytes, a `responseType` this build does not decode is the parse fault
665
+ * `ocsp/unsupported-response-type`. A record that carries such a response, which RFC 5940 sec. 3
666
+ * permits a message to carry, is taken and determines nothing: the verdict is `"unknown"` with no
667
+ * `signatureValid`, because the body stating the certificate's status was never read.
668
+ *
662
669
  * @opts
663
670
  * cert the target certificate (parsed, DER, or PEM) -- REQUIRED.
664
671
  * issuer its issuer certificate (parsed, DER, or PEM) -- REQUIRED.
package/lib/oid.js CHANGED
@@ -432,6 +432,13 @@ var FAMILIES = Object.assign(Object.create(null), {
432
432
  scepMessageType: 2, scepPkiStatus: 3, scepFailInfo: 4,
433
433
  scepSenderNonce: 5, scepRecipientNonce: 6, scepTransactionId: 7 } },
434
434
  scep: { base: [1, 3, 6, 1, 5, 5, 7, 24], of: { scepFailInfoText: 1 } },
435
+ /** @internal id-ri, the RFC 5940 revocation-information arc. Only the two leaves a reader can act
436
+ * on are registered: 2 names an OCSPResponse carried in a CMS `crls` field (RFC 5940 sec. 2) and
437
+ * 4 names an SCVP request-response pair (sec. 4). Leaves 1 and 3 are the SCVP sub-arcs RFC 5940
438
+ * does not define a CMS carriage for. The hyphenated member names are the RFC's own; `ocspResponse`
439
+ * is already bound to the OCSP nonce arc's `1.3.6.1.5.5.7.48.1.4` and is read by name from the
440
+ * OCSP extension-placement table, so reusing it here would redirect that table as well. */
441
+ idRi: { base: [1, 3, 6, 1, 5, 5, 7, 16], of: { "id-ri-ocsp-response": 2, "id-ri-scvp": 4 } },
435
442
  });
436
443
 
437
444
  var _byOid = new Map();
package/lib/schema-cms.js CHANGED
@@ -34,8 +34,13 @@
34
34
  * (sec. 5.4); `authAttrsBytes` plays the same role for the sec. 9.2 MAC input and the
35
35
  * RFC 5083 sec. 2.2 AAD. Embedded certificates and CRLs are surfaced as raw DER +
36
36
  * their outer tag, validated against the closed CertificateChoices /
37
- * RevocationInfoChoice tag sets, so an obsolete alternative never fails the
38
- * parse but an out-of-set element does. DER-only, fail-closed.
37
+ * RevocationInfoChoice tag sets, so an out-of-set element fails the parse. A
38
+ * certificate alternative is surfaced raw whatever it holds; a `crls` entry in the
39
+ * `other [1]` arm is read as RFC 5652 sec. 10.2.1 `OtherRevocationInfoFormat`,
40
+ * which names its format and, for RFC 5940 `id-ri-ocsp-response`, carries the
41
+ * embedded OCSP response as a record revocation checking takes directly. An
42
+ * unrecognized format stays opaque; a malformed one fails the parse. DER-only,
43
+ * fail-closed.
39
44
  *
40
45
  * @card
41
46
  * Parse DER / PEM CMS (RFC 5652 / 5083 / 9629) into structured, validated fields:
@@ -49,6 +54,9 @@ var schema = require("./schema-engine");
49
54
  var pkix = require("./schema-pkix");
50
55
  var guard = require("./guard-all");
51
56
  var oid = require("./oid");
57
+ /** @internal The OCSP response schema, for the RFC 5940 `crls` arm. `schema-ocsp` requires neither
58
+ * this module nor anything that reaches it, so this is an ordinary top-of-file require. */
59
+ var ocspSchema = require("./schema-ocsp");
52
60
  var frameworkError = require("./framework-error");
53
61
  var schemaX509 = require("./schema-x509");
54
62
  var schemaCrl = require("./schema-crl");
@@ -383,6 +391,78 @@ var CRL_CHOICE_TAGS = Object.assign(Object.create(null), {
383
391
  universal: Object.create(null),
384
392
  context: Object.assign(Object.create(null), { 1: true }),
385
393
  });
394
+
395
+ /** @internal RFC 5652 sec. 10.2.1's `other` arm:
396
+ *
397
+ * OtherRevocationInfoFormat ::= SEQUENCE { otherRevInfoFormat OBJECT IDENTIFIER, otherRevInfo ANY }
398
+ *
399
+ * Exactly two components. OpenSSL's `crypto/cms/cms_asn1.c` makes `otherRevInfo` optional, so it both
400
+ * accepts and can emit an entry carrying no revocation information at all; the ASN.1 above has no
401
+ * OPTIONAL and the one-element form is refused here. */
402
+ var OTHER_REV_INFO = schema.seq([
403
+ schema.field("otherRevInfoFormat", schema.oidLeaf()),
404
+ schema.field("otherRevInfo", schema.any()),
405
+ ], {
406
+ assert: "implicit", implicitTag: 1, arity: { exact: 2 },
407
+ code: "cms/bad-other-rev-info", what: "OtherRevocationInfoFormat",
408
+ build: function (m) {
409
+ return {
410
+ otherRevInfoFormat: m.fields.otherRevInfoFormat.value,
411
+ otherRevInfoNode: m.fields.otherRevInfo.value,
412
+ };
413
+ },
414
+ });
415
+ var OID_ID_RI_OCSP_RESPONSE = oid.byName("id-ri-ocsp-response");
416
+
417
+ /** @internal The crls element mapper, which unlike `rawChoiceElement` needs the walk's own ctx: the
418
+ * `other` arm's interior is decoded under the caller's caps and budget rather than at defaults.
419
+ *
420
+ * The universal arm is surfaced raw as before, with one new refusal: RFC 5940 sec. 2 states that an
421
+ * OCSP response is carried in the `other` arm, so a response placed where a CertificateList belongs
422
+ * is a smuggle. It is detected POSITIVELY, by the OCSP module's own response detector, because the
423
+ * CRL detector answers false for shapes that are not full CertificateLists and "not a CRL" would
424
+ * therefore refuse more than sec. 2 forbids. */
425
+ /** @internal The mapper bound to one walk's ctx, so it fits the one-argument shape a list projection
426
+ * passes. The ctx is the walk's own, not a fresh one: that is what carries the caller's caps and the
427
+ * budget already being spent into the embedded response. */
428
+ function _mapCrlsWith(ctx, code, what) {
429
+ var mapper = crlChoiceElement(code, what);
430
+ return function (item) { return mapper(item, ctx); };
431
+ }
432
+ function crlChoiceElement(code, what) {
433
+ return function (item, ctx) {
434
+ var n = item.node;
435
+ var byClass = CRL_CHOICE_TAGS[n.tagClass];
436
+ if (!n.constructed || !byClass || byClass[n.tagNumber] !== true) {
437
+ throw NS.E(code, what + " element tag is outside the closed CHOICE set (RFC 5652 sec. 10.2.1-sec. 10.2.2)");
438
+ }
439
+ var out = rawElement(item);
440
+ if (n.tagClass !== "context") {
441
+ if (ocspSchema.matchesResponse(n)) {
442
+ throw NS.E("cms/misplaced-ocsp-response", "an OCSP response is carried in the other [1] " +
443
+ "RevocationInfoChoice arm under id-ri-ocsp-response, not in the CertificateList arm (RFC 5940 sec. 2)");
444
+ }
445
+ return out;
446
+ }
447
+ var other = schema.walk(OTHER_REV_INFO, n, ctx).result;
448
+ out.otherRevInfoFormat = other.otherRevInfoFormat;
449
+ out.otherRevInfoFormatName = ctx.oid.name(other.otherRevInfoFormat) || null;
450
+ out.otherRevInfoBytes = other.otherRevInfoNode.bytes;
451
+ out.ocspResponse = null;
452
+ if (other.otherRevInfoFormat === OID_ID_RI_OCSP_RESPONSE) {
453
+ var resp = ocspSchema.walkOcspResponse(other.otherRevInfoNode, ctx);
454
+ /** @internal RFC 5940 sec. 3: the carried response states a status. A `tryLater` or `malformed`
455
+ * OCSPResponse is a VALID OCSPResponse that conveys no revocation information, so it is refused
456
+ * HERE rather than in the OCSP module, whose standalone door must keep accepting it. */
457
+ if (resp.responseStatus.code !== 0) {
458
+ throw NS.E("cms/bad-ocsp-rev-info", "an id-ri-ocsp-response revocation entry carries a " +
459
+ resp.responseStatus.name + " OCSP response, which states no revocation status (RFC 5940 sec. 3)");
460
+ }
461
+ out.ocspResponse = resp;
462
+ }
463
+ return out;
464
+ };
465
+ }
386
466
  CERT_CHOICE_TAGS.universal[asn1.TAGS.SEQUENCE] = true;
387
467
  CRL_CHOICE_TAGS.universal[asn1.TAGS.SEQUENCE] = true;
388
468
  function rawChoiceElement(allowed, code, what) {
@@ -648,7 +728,8 @@ var SIGNED_DATA = schema.seq([
648
728
  var version = m.fields.version.value;
649
729
  var encapContentInfo = m.fields.encapContentInfo.value.result;
650
730
  var certificates = m.fields.certificates.present ? m.fields.certificates.value.items.map(rawChoiceElement(CERT_CHOICE_TAGS, "cms/bad-certificates", "certificates")) : [];
651
- var crls = m.fields.crls.present ? m.fields.crls.value.items.map(rawChoiceElement(CRL_CHOICE_TAGS, "cms/bad-crls", "crls")) : [];
731
+ var crls = m.fields.crls.present
732
+ ? guard.list.copyMap(m.fields.crls.value.items, _mapCrlsWith(ctx, "cms/bad-crls", "crls")) : [];
652
733
  var signerInfos = m.fields.signerInfos.value.items.map(function (it) { return it.value.result; });
653
734
 
654
735
  if (encapContentInfo.eContentType !== OID_DATA) {
@@ -909,10 +990,16 @@ var ORIGINATOR_INFO = schema.seq([
909
990
  schema.optional("crls", schema.implicitSetOf(1, schema.any(), { min: 1, code: "cms/bad-originator-crls", what: "crls" }), { tag: 1 }),
910
991
  ], {
911
992
  assert: "constructed", code: "cms/bad-originator-info", what: "OriginatorInfo",
912
- build: function (m) {
993
+ /** @internal The build takes `ctx` because the crls mapper decodes the RFC 5940 arm under the
994
+ * caller's own caps and budget. This one record is shared by EnvelopedData (RFC 5652 sec. 6.1),
995
+ * AuthenticatedData (sec. 9.1) and AuthEnvelopedData (RFC 5083 sec. 2.1), so decoding only in
996
+ * SignedData would leave three of the four content types RFC 5940 sec. 1 names undecoded. */
997
+ build: function (m, ctx) {
913
998
  return {
914
999
  certs: m.fields.certs.present ? m.fields.certs.value.items.map(rawChoiceElement(CERT_CHOICE_TAGS, "cms/bad-originator-certs", "OriginatorInfo certs")) : [],
915
- crls: m.fields.crls.present ? m.fields.crls.value.items.map(rawChoiceElement(CRL_CHOICE_TAGS, "cms/bad-originator-crls", "OriginatorInfo crls")) : [],
1000
+ crls: m.fields.crls.present
1001
+ ? guard.list.copyMap(m.fields.crls.value.items,
1002
+ _mapCrlsWith(ctx, "cms/bad-originator-crls", "OriginatorInfo crls")) : [],
916
1003
  };
917
1004
  },
918
1005
  });
@@ -1184,7 +1271,14 @@ var CONTENT_INFO = schema.seq([
1184
1271
  * unauthAttrs }` (`aead` holds the validated AES-GCM/CCM nonce + ICV length, or
1185
1272
  * `null` for an unrecognized algorithm). A KEM recipient (RFC 9629) surfaces as
1186
1273
  * `{ type: "ori", oriType, oriValue, kemri }` with the parsed KEMRecipientInfo in
1187
- * `kemri`. Every result additionally carries `contentType` (the dotted OID) and
1274
+ * `kemri`. A `crls` entry is `{ bytes, tagClass, tagNumber }` for a CertificateList;
1275
+ * an entry in the `other [1]` arm additionally carries `otherRevInfoFormat` (dotted),
1276
+ * `otherRevInfoFormatName` (or `null`), `otherRevInfoBytes` (the raw ANY TLV) and,
1277
+ * for RFC 5940 `id-ri-ocsp-response`, `ocspResponse` holding the embedded response
1278
+ * as a record `pki.path.ocspChecker` and `pki.path.verifyOcspResponse` take
1279
+ * directly. A response whose `responseType` is one this build does not decode keeps
1280
+ * its envelope in `ocspResponse` with `basicResponse` null and the body in
1281
+ * `responseBytes.response` as it arrived. `originatorInfo.crls` carries the same shapes. Every result additionally carries `contentType` (the dotted OID) and
1188
1282
  * `contentTypeName` (its registry name) naming which of the five shapes was
1189
1283
  * dispatched. The raw byte ranges an external verifier hashes (`eContent`,
1190
1284
  * `signature`, `signedAttrsBytes`, `authAttrsBytes`, `mac`) are surfaced
@@ -1337,16 +1431,27 @@ function parseCertsOnly(der, E, prefix, maxCerts, requireCert) {
1337
1431
  } else if (allCerts.length === 0 && allCrls.length === 0) {
1338
1432
  throw E(prefix + "/no-certificates", "a certs-only message must contain at least one certificate or CRL (RFC 8551 sec. 3.8)");
1339
1433
  }
1434
+ /** @internal `maxCerts` caps the certificates and CRLs PARSED and RETURNED, which is a resource
1435
+ * bound. It may not decide which messages are accepted. The tag-class test reads a field off an
1436
+ * entry the parse already produced and costs nothing, so it runs over EVERY entry before the cap
1437
+ * is applied: taken after it, a cap leaving no room for the crls dropped a tagged entry silently,
1438
+ * so a capped reader accepted a message an uncapped reader refuses and handed back an empty crls
1439
+ * list where revocation information had been. Only the X.509 and CRL parsing below stays bounded,
1440
+ * which is the work the bound exists for. */
1441
+ for (var ci = 0; ci < allCerts.length; ci++) {
1442
+ if (allCerts[ci].tagClass !== "universal") throw E(prefix + "/bad-certificate-choice", "a certs-only response exchanges plain X.509 certificates; a tagged CertificateChoices alternative is not permitted (RFC 5272)");
1443
+ }
1444
+ for (var cj = 0; cj < allCrls.length; cj++) {
1445
+ if (allCrls[cj].tagClass !== "universal") throw E(prefix + "/bad-crl", "a certs-only response CRL must be a plain X.509 CertificateList, not a tagged otherRevInfo alternative (RFC 5652 sec. 10.2.1)");
1446
+ }
1340
1447
  var certs = (maxCerts != null && allCerts.length > maxCerts) ? allCerts.slice(0, maxCerts) : allCerts;
1341
1448
  for (var i = 0; i < certs.length; i++) {
1342
- if (certs[i].tagClass !== "universal") throw E(prefix + "/bad-certificate-choice", "a certs-only response exchanges plain X.509 certificates; a tagged CertificateChoices alternative is not permitted (RFC 5272)");
1343
1449
  try { schemaX509.parse(certs[i].bytes); }
1344
1450
  catch (e) { throw E(prefix + "/bad-certificate", "a certs-only response carried a non-certificate in its certificates field (RFC 5272 sec. 4.1)", e); }
1345
1451
  }
1346
1452
  var crlCap = maxCerts != null ? (maxCerts - certs.length) : null;
1347
1453
  var crls = (crlCap != null && allCrls.length > crlCap) ? allCrls.slice(0, crlCap) : allCrls;
1348
1454
  for (var j = 0; j < crls.length; j++) {
1349
- if (crls[j].tagClass !== "universal") throw E(prefix + "/bad-crl", "a certs-only response CRL must be a plain X.509 CertificateList, not a tagged otherRevInfo alternative (RFC 5652 sec. 10.2.1)");
1350
1455
  try { schemaCrl.parse(crls[j].bytes); }
1351
1456
  catch (e) { throw E(prefix + "/bad-crl", "a certs-only response carried a non-CRL in its crls field", e); }
1352
1457
  }
@@ -40,6 +40,7 @@ var oid = require("./oid");
40
40
  var constants = require("./constants");
41
41
  var frameworkError = require("./framework-error");
42
42
  var intrinsic = require("./guard-intrinsic");
43
+ var guard = require("./guard-all");
43
44
  var _hasOwn = intrinsic.hasOwn;
44
45
  var _String = intrinsic.String;
45
46
 
@@ -319,18 +320,24 @@ var RESPONSE_BYTES = schema.seq([
319
320
  schema.field("response", schema.octetString()),
320
321
  ], {
321
322
  assert: "sequence", arity: { exact: 2 }, code: "ocsp/bad-response-bytes", what: "ResponseBytes",
323
+ /** @internal RFC 6960 sec. 4.2.1 dispatches the body on `responseType`, so a type this build does
324
+ * not read leaves `basicResponse` null and the body in `response` exactly as it arrived. The
325
+ * envelope is the part every reader shares, and which bodies this build decodes is its own support
326
+ * limit rather than a shape rule: `parseResponse` refuses an unread body at its door, where the
327
+ * caller asked for a response to read, and a response carried inside another structure stays
328
+ * readable as far as its envelope goes. A body that claims id-pkix-ocsp-basic is still decoded,
329
+ * and still refused when it is not one. */
322
330
  build: function (m, ctx) {
323
331
  var responseType = m.fields.responseType.value;
324
332
  var raw = m.fields.response.value;
325
- if (responseType !== OID_OCSP_BASIC) {
326
- throw NS.E("ocsp/unsupported-response-type", (ctx.oid.name(responseType) || responseType) + " is not id-pkix-ocsp-basic (RFC 6960 sec. 4.2.1)");
327
- }
328
333
  return {
329
334
  responseType: responseType,
330
335
  responseTypeName: ctx.oid.name(responseType) || null,
331
336
  response: raw,
332
- basicResponse: schema.embeddedDer(BASIC_OCSP_RESPONSE, raw, ctx,
333
- { code: "ocsp/bad-der", what: "BasicOCSPResponse" }).result,
337
+ basicResponse: responseType === OID_OCSP_BASIC
338
+ ? schema.embeddedDer(BASIC_OCSP_RESPONSE, raw, ctx,
339
+ { code: "ocsp/bad-der", what: "BasicOCSPResponse" }).result
340
+ : null,
334
341
  };
335
342
  },
336
343
  });
@@ -506,7 +513,19 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
506
513
  * above it, or an option outside this set, is refused. A parse that exceeds one is
507
514
  * refused with the `/too-large` code of this format.
508
515
  */
509
- var parseResponse = pkix.makeRecordingParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS }, "ocspResponse");
516
+ var _parseResponseRecorded = pkix.makeRecordingParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS }, "ocspResponse");
517
+ /** @internal The decision is made on `responseType`, the field RFC 6960 sec. 4.2.1 dispatches on,
518
+ * rather than on the absent `basicResponse` that follows from it. A non-successful response carries
519
+ * no responseBytes at all and keeps reaching the caller, which is the status a live responder
520
+ * returns. */
521
+ function parseResponse(input, caps) {
522
+ var out = _parseResponseRecorded(input, caps);
523
+ var rb = out.responseBytes;
524
+ if (rb && rb.responseType !== OID_OCSP_BASIC) {
525
+ throw NS.E("ocsp/unsupported-response-type", (rb.responseTypeName || rb.responseType) + " is not id-pkix-ocsp-basic (RFC 6960 sec. 4.2.1)");
526
+ }
527
+ return out;
528
+ }
510
529
 
511
530
  /**
512
531
  * @primitive pki.schema.ocsp.pemDecode
@@ -580,9 +599,25 @@ function matchesRequest(root) {
580
599
  return true;
581
600
  }
582
601
 
602
+ /** @internal Walk an ALREADY-DECODED OCSPResponse node under the caller's own parse resources, for a
603
+ * carrier that embeds one: RFC 5940 sec. 2 puts an OCSPResponse in a CMS `crls` field, and the CMS
604
+ * parse reaches it with a node it has already decoded and a budget it is already spending.
605
+ * `parseResponse(bytes)` cannot serve that: it re-decodes from scratch and starts a fresh budget, so
606
+ * a caller's tightened caps would not reach the embedded walk. `withResources` carries the caps and
607
+ * the budget while keeping this module's own error class, so a malformed embedded response still
608
+ * reports an `ocsp/*` code. The record is a recording one, so `guard.parsed.fromTrustedSource`
609
+ * accepts it wherever a parsed response is taken and a rebuilt copy is still refused. The re-decode
610
+ * this walker performs on raw DER is STRICT, unlike the CMS one: an OCSPResponse has no
611
+ * BER-tolerant door. */
612
+ var walkOcspResponse = guard.parsed.recordingWalker("ocspResponse", function (node, ctx) {
613
+ return schema.walk(OCSP_RESPONSE, node, schema.withResources(NS, ctx)).result;
614
+ // allow:bare-decode the re-decode a recording walker performs on raw DER a caller hands it, outside any parse.
615
+ }, function (der) { return asn1.decode(der); });
616
+
583
617
  module.exports = {
584
618
  parseRequest: parseRequest,
585
619
  parseResponse: parseResponse,
620
+ walkOcspResponse: walkOcspResponse,
586
621
  pemDecode: pemDecode,
587
622
  pemEncode: pemEncode,
588
623
  matchesRequest: matchesRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.56",
3
+ "version": "0.8.58",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:921f4ed0-ec38-4c9d-b490-d6223c040e96",
5
+ "serialNumber": "urn:uuid:41653943-4244-48b6-acbe-1493b54bad90",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-06T23:49:00.685Z",
8
+ "timestamp": "2026-10-07T04:10:13.613Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.8.56",
22
+ "bom-ref": "@blamejs/pki@0.8.58",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.56",
25
+ "version": "0.8.58",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.8.56",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.58",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.8.56",
57
+ "ref": "@blamejs/pki@0.8.58",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]