@blamejs/pki 0.1.17 → 0.1.19

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/schema-cms.js CHANGED
@@ -132,12 +132,11 @@ function rawElement(item) {
132
132
  // [3], a v2AttrCert [2], a v1AttrCert [1]; a RevocationInfoChoice `other` is [1]),
133
133
  // the SignerInfo versions, and whether the content type is id-data.
134
134
  function _expectedSignedDataVersion(certificates, crls, signerInfos, eContentType) {
135
- function ctx(el, n) { return el.tagClass === "context" && el.tagNumber === n; }
136
- var otherCert = certificates.some(function (c) { return ctx(c, 3); });
137
- var otherCrl = crls.some(function (c) { return ctx(c, 1); });
135
+ var otherCert = certificates.some(function (c) { return schema.isContext(c, 3); });
136
+ var otherCrl = crls.some(function (c) { return schema.isContext(c, 1); });
138
137
  if (otherCert || otherCrl) return 5;
139
- if (certificates.some(function (c) { return ctx(c, 2); })) return 4; // v2AttrCert [2]
140
- if (certificates.some(function (c) { return ctx(c, 1); }) || // v1AttrCert [1]
138
+ if (certificates.some(function (c) { return schema.isContext(c, 2); })) return 4; // v2AttrCert [2]
139
+ if (certificates.some(function (c) { return schema.isContext(c, 1); }) || // v1AttrCert [1]
141
140
  signerInfos.some(function (si) { return si.version === 3; }) ||
142
141
  eContentType !== OID_DATA) return 3;
143
142
  return 1;
@@ -528,12 +527,11 @@ var ORIGINATOR_INFO = schema.seq([
528
527
  // [2]; a crl `other` is [1]; a pwri or ori forces v3; all-ktri-IAS with no
529
528
  // originatorInfo/unprotectedAttrs is v0; everything else v2).
530
529
  function _expectedEnvelopedDataVersion(originatorInfo, recipientInfos, hasUnprotectedAttrs) {
531
- function ctx(el, n) { return el.tagClass === "context" && el.tagNumber === n; }
532
530
  var hasOrig = !!originatorInfo;
533
531
  var certs = hasOrig ? originatorInfo.certs : [];
534
532
  var crls = hasOrig ? originatorInfo.crls : [];
535
- if (hasOrig && (certs.some(function (c) { return ctx(c, 3); }) || crls.some(function (c) { return ctx(c, 1); }))) return 4;
536
- if ((hasOrig && certs.some(function (c) { return ctx(c, 2); })) ||
533
+ if (hasOrig && (certs.some(function (c) { return schema.isContext(c, 3); }) || crls.some(function (c) { return schema.isContext(c, 1); }))) return 4;
534
+ if ((hasOrig && certs.some(function (c) { return schema.isContext(c, 2); })) ||
537
535
  recipientInfos.some(function (r) { return r.type === "pwri" || r.type === "ori"; })) return 3;
538
536
  if (!hasOrig && !hasUnprotectedAttrs &&
539
537
  recipientInfos.every(function (r) { return r.type === "ktri" && r.ridType === "issuerAndSerialNumber"; })) return 0;
@@ -676,11 +674,10 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CMS", PemE
676
674
  // registry order.
677
675
  function matches(root) {
678
676
  var TAGS = asn1.TAGS;
679
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
680
- var k = root.children;
681
- if (!k || k.length !== 2) return false;
682
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.OBJECT_IDENTIFIER)) return false;
683
- if (!(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
677
+ var k = pkix.rootSequenceChildren(root, 2, 2);
678
+ if (!k) return false;
679
+ if (!schema.isUniversal(k[0], TAGS.OBJECT_IDENTIFIER)) return false;
680
+ if (!(schema.isContext(k[1], 0) && k[1].children)) return false;
684
681
  return true;
685
682
  }
686
683
 
@@ -692,10 +689,20 @@ function matches(root) {
692
689
  // universal SEQUENCE (an IMPLICIT [n] EnvelopedData is retagged by the caller).
693
690
  function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).result; }
694
691
 
692
+ // Validate a bare SignedData / EncryptedData node the same way — for a composer
693
+ // that holds an already-decoded content node and must not re-decode its bytes
694
+ // (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
695
+ // that the strict `parse` entry would refuse). Same contract as
696
+ // walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
697
+ function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
698
+ function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
699
+
695
700
  module.exports = {
696
701
  parse: parse,
697
702
  pemDecode: pemDecode,
698
703
  pemEncode: pemEncode,
699
704
  matches: matches,
700
705
  walkEnvelopedData: walkEnvelopedData,
706
+ walkSignedData: walkSignedData,
707
+ walkEncryptedData: walkEncryptedData,
701
708
  };
package/lib/schema-crl.js CHANGED
@@ -238,10 +238,10 @@ function matches(root) {
238
238
  // version, then signature (SEQUENCE) + issuer (SEQUENCE); the next element is
239
239
  // thisUpdate (Time) for a CRL, Validity (SEQUENCE) for a certificate.
240
240
  var i = 0;
241
- if (tbs.children[i] && tbs.children[i].tagClass === "universal" && tbs.children[i].tagNumber === TAGS.INTEGER) i++;
241
+ if (schema.isUniversal(tbs.children[i], TAGS.INTEGER)) i++;
242
242
  i += 2; // signature + issuer
243
243
  var pos = tbs.children[i];
244
- return !!pos && pos.tagClass === "universal" && (pos.tagNumber === TAGS.UTC_TIME || pos.tagNumber === TAGS.GENERALIZED_TIME);
244
+ return schema.isUniversalOneOf(pos, [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME]);
245
245
  }
246
246
 
247
247
  module.exports = {
@@ -216,7 +216,7 @@ function popoPrivKey(type) {
216
216
  }
217
217
  }
218
218
  if (inner.tagNumber === 3) {
219
- try { schema.walk(PKMAC_VALUE, asn1.decode(asn1.sequenceTlv(inner)), NS); }
219
+ try { schema.embeddedDer(PKMAC_VALUE, asn1.sequenceTlv(inner), NS, { code: "crmf/bad-popo", what: "agreeMAC [3] PKMACValue" }); }
220
220
  catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 §4.2)", e); }
221
221
  }
222
222
  // encryptedKey [4] EnvelopedData — validate the CMS structure fail-closed by
@@ -296,30 +296,27 @@ var CERT_TEMPLATE = schema.seq([
296
296
  assert: "sequence", code: "crmf/bad-cert-template", what: "CertTemplate",
297
297
  build: function (m, ctx) {
298
298
  var f = m.fields;
299
- // RFC 4211 §5 serialNumber, signingAlg, issuerUID, and subjectUID MUST be
300
- // omitted from a CertTemplate: serialNumber and signingAlg are assigned by the
301
- // CA, and the UID pair is deprecated. A requester must not dictate a
302
- // CA-assigned value (a requester-chosen serialNumber is a real hazard), so a
303
- // template that sets any of them is rejected fail-closed, not surfaced as
304
- // acceptable. They stay in the field map (parsed, then rejected) so the
305
- // diagnostic names the offending field rather than a bare tag number.
306
- var caAssigned = ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
307
- for (var i = 0; i < caAssigned.length; i++) {
308
- if (f[caAssigned[i]].present) {
309
- throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
310
- }
311
- }
312
- // version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5).
299
+ // version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5). This
300
+ // is a structural CertTemplate rule (it holds wherever the template appears);
301
+ // the CA-assigned-field omission rule is REQUEST-context-specific and lives in
302
+ // CERT_REQUEST.build, because the same CertTemplate structure identifies an
303
+ // existing certificate serialNumber and issuer present inside a CMP
304
+ // RevDetails (RFC 9810 §5.3.9). Every field is surfaced so both the request
305
+ // omission check and the revocation consumer read them off one result.
313
306
  if (f.version.present && f.version.value !== 2n) {
314
307
  throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 §5)");
315
308
  }
316
309
  return {
317
- version: f.version.present ? f.version.value : null,
318
- issuer: f.issuer.present ? f.issuer.value : null,
319
- validity: f.validity.present ? f.validity.value.result : null,
320
- subject: f.subject.present ? f.subject.value : null,
321
- publicKey: f.publicKey.present ? f.publicKey.value.result : null,
322
- extensions: f.extensions.present ? f.extensions.value.result : null,
310
+ version: f.version.present ? f.version.value : null,
311
+ serialNumber: f.serialNumber.present ? f.serialNumber.value : null,
312
+ signingAlg: f.signingAlg.present ? f.signingAlg.value.result : null,
313
+ issuer: f.issuer.present ? f.issuer.value : null,
314
+ validity: f.validity.present ? f.validity.value.result : null,
315
+ subject: f.subject.present ? f.subject.value : null,
316
+ publicKey: f.publicKey.present ? f.publicKey.value.result : null,
317
+ issuerUID: f.issuerUID.present ? f.issuerUID.value : null,
318
+ subjectUID: f.subjectUID.present ? f.subjectUID.value : null,
319
+ extensions: f.extensions.present ? f.extensions.value.result : null,
323
320
  };
324
321
  },
325
322
  });
@@ -334,11 +331,32 @@ var CERT_REQUEST = schema.seq([
334
331
  schema.optional("controls", CONTROLS, { whenUniversal: [TAGS.SEQUENCE] }),
335
332
  ], {
336
333
  assert: "sequence", code: "crmf/bad-cert-request", what: "CertRequest",
337
- build: function (m) {
334
+ build: function (m, ctx) {
335
+ var tpl = m.fields.certTemplate.value.result;
336
+ // RFC 4211 §5 — serialNumber, signingAlg, issuerUID, and subjectUID MUST be
337
+ // omitted from a REQUEST's CertTemplate: serialNumber and signingAlg are
338
+ // assigned by the CA, and the UID pair is deprecated. A requester must not
339
+ // dictate a CA-assigned value (a requester-chosen serialNumber is a real
340
+ // hazard), so a request template that sets any of them is rejected
341
+ // fail-closed. The check lives HERE, not in CERT_TEMPLATE, because the same
342
+ // structure legitimately carries serialNumber/issuer in a CMP RevDetails.
343
+ // A cross-certification request (ccr) is the ONE exception: RFC 9810 Appendix
344
+ // D.6 profiles its template with signingAlg PRESENT (the requesting CA states
345
+ // the algorithm it wants the cross-certificate signed with), so the caller
346
+ // relaxes that single field via ctx.allowSigningAlg; serialNumber and the UID
347
+ // pair stay forbidden.
348
+ var caAssigned = ctx.allowSigningAlg
349
+ ? ["serialNumber", "issuerUID", "subjectUID"]
350
+ : ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
351
+ for (var i = 0; i < caAssigned.length; i++) {
352
+ if (tpl[caAssigned[i]] !== null) {
353
+ throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
354
+ }
355
+ }
338
356
  return {
339
357
  certReqId: m.fields.certReqId.value,
340
358
  certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
341
- certTemplate: m.fields.certTemplate.value.result,
359
+ certTemplate: tpl,
342
360
  controls: m.fields.controls.present ? m.fields.controls.value.result : null,
343
361
  certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (§4.1)
344
362
  };
@@ -466,20 +484,36 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, Pem
466
484
  // ocsp-request: an OCSPRequest's tbsRequest never leads with the INTEGER-then-
467
485
  // SEQUENCE pair (its children[0] is a context [0] version or a requestList).
468
486
  function matches(root) {
469
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children || root.children.length < 1) return false;
470
- var msg = root.children[0];
471
- if (!msg.children || msg.tagClass !== "universal" || msg.tagNumber !== TAGS.SEQUENCE || msg.children.length < 1) return false;
487
+ var k = pkix.rootSequenceChildren(root, 1);
488
+ if (!k) return false;
489
+ var msg = k[0];
490
+ if (!(msg.children && schema.isUniversal(msg, TAGS.SEQUENCE) && msg.children.length >= 1)) return false;
472
491
  var certReq = msg.children[0];
473
- if (!certReq.children || certReq.tagClass !== "universal" || certReq.tagNumber !== TAGS.SEQUENCE || certReq.children.length < 2) return false;
492
+ if (!(certReq.children && schema.isUniversal(certReq, TAGS.SEQUENCE) && certReq.children.length >= 2)) return false;
474
493
  var id = certReq.children[0], tpl = certReq.children[1];
475
- return id.tagClass === "universal" && id.tagNumber === TAGS.INTEGER &&
476
- tpl.tagClass === "universal" && tpl.tagNumber === TAGS.SEQUENCE && !!tpl.children;
494
+ return schema.isUniversal(id, TAGS.INTEGER) &&
495
+ schema.isUniversal(tpl, TAGS.SEQUENCE) && !!tpl.children;
496
+ }
497
+
498
+ // Validate a bare, already-decoded CertReqMessages / CertTemplate node — for a
499
+ // composer that carries these structures inside its own envelope (an RFC 9810
500
+ // CMP PKIBody's ir/cr/kur/krr/ccr arms; a RevDetails.certDetails). NS-bound to
501
+ // crmf so an interior failure throws CrmfError with a crmf/* code, never the
502
+ // composer's class wearing a foreign code. Same contract as the CMS walkers.
503
+ // opts.allowSigningAlg relaxes the request-template signingAlg-omission rule for
504
+ // the CMP cross-certification (ccr) arm only (RFC 9810 Appendix D.6).
505
+ function walkCertReqMessages(node, opts) {
506
+ var ctx = (opts && opts.allowSigningAlg) ? Object.assign({}, NS, { allowSigningAlg: true }) : NS;
507
+ return schema.walk(CERT_REQ_MESSAGES, node, ctx).result;
477
508
  }
509
+ function walkCertTemplate(node) { return schema.walk(CERT_TEMPLATE, node, NS).result; }
478
510
 
479
511
  module.exports = {
480
512
  parse: parse,
481
513
  pemDecode: pemDecode,
482
514
  matches: matches,
515
+ walkCertReqMessages: walkCertReqMessages,
516
+ walkCertTemplate: walkCertTemplate,
483
517
  // The top schema, exposed (not on the curated pki.schema.crmf surface) so a
484
518
  // round-trip test can prove schema.encode(schema, v) -> decode -> parse recovers
485
519
  // v — the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
package/lib/schema-csr.js CHANGED
@@ -171,10 +171,10 @@ function matches(root) {
171
171
  if (!cri) return false;
172
172
  var k = cri.children;
173
173
  if (k.length !== 4) return false;
174
- return k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER &&
175
- k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE &&
176
- k[2].tagClass === "universal" && k[2].tagNumber === TAGS.SEQUENCE &&
177
- k[3].tagClass === "context" && k[3].tagNumber === 0;
174
+ return schema.isUniversal(k[0], TAGS.INTEGER) &&
175
+ schema.isUniversal(k[1], TAGS.SEQUENCE) &&
176
+ schema.isUniversal(k[2], TAGS.SEQUENCE) &&
177
+ schema.isContext(k[3], 0);
178
178
  }
179
179
 
180
180
  module.exports = {
@@ -160,10 +160,10 @@ function optional(name, schema, opts) {
160
160
  var match = opts.whenAny
161
161
  ? function () { return true; }
162
162
  : opts.whenUniversal
163
- ? function (n) { return n.tagClass === "universal" && opts.whenUniversal.indexOf(n.tagNumber) !== -1; }
163
+ ? function (n) { return isUniversalOneOf(n, opts.whenUniversal); }
164
164
  : opts.tags
165
- ? function (n) { return n.tagClass === "context" && opts.tags.indexOf(n.tagNumber) !== -1; }
166
- : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
165
+ ? function (n) { return isContextOneOf(n, opts.tags); }
166
+ : function (n) { return isContext(n, opts.tag); };
167
167
  return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
168
168
  explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
169
169
  }
@@ -200,12 +200,14 @@ function choice(alts, opts) {
200
200
  function seqOf(item, opts) {
201
201
  opts = opts || {};
202
202
  return { kind: "repeat", item: item, assert: opts.assert || "sequence", code: opts.code, what: opts.what,
203
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
203
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
204
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
204
205
  }
205
206
  function setOf(item, opts) {
206
207
  opts = opts || {};
207
208
  return { kind: "repeat", item: item, assert: opts.assert || "set", derSetOrder: true, code: opts.code, what: opts.what,
208
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
209
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
210
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
209
211
  }
210
212
  function setOfUnique(item, keyFn, opts) {
211
213
  return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
@@ -216,7 +218,8 @@ function setOfUnique(item, keyFn, opts) {
216
218
  function implicitSetOf(tag, item, opts) {
217
219
  opts = opts || {};
218
220
  return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
219
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
221
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
222
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
220
223
  }
221
224
  // [tag] IMPLICIT SEQUENCE OF item — the context tag REPLACES the universal SEQUENCE
222
225
  // tag, so the node is a context-class constructed [tag] whose direct children are the
@@ -225,7 +228,8 @@ function implicitSetOf(tag, item, opts) {
225
228
  function implicitSeqOf(tag, item, opts) {
226
229
  opts = opts || {};
227
230
  return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, code: opts.code, what: opts.what,
228
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
231
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
232
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
229
233
  }
230
234
 
231
235
  // ---- the walk engine -------------------------------------------------
@@ -304,6 +308,12 @@ function _walkRepeat(schema, node, ctx) {
304
308
  if (schema.min != null && kids.length < schema.min) {
305
309
  _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
306
310
  }
311
+ // The element-count ceiling: a container of a great many tiny elements
312
+ // amplifies memory through per-element walk products, so a schema that
313
+ // parses attacker-sized lists declares `max` and fails typed instead.
314
+ if (schema.max != null && kids.length > schema.max) {
315
+ _fail(ctx, schema.maxCode || schema.code, (schema.what || "value") + " exceeds the element cap " + schema.max);
316
+ }
307
317
  // DER (X.690 §11.6) — the components of a SET OF appear in ascending order, the
308
318
  // encodings compared as octet strings. SEQUENCE OF is order-preserving and is
309
319
  // exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
@@ -500,6 +510,9 @@ function _encodeRepeat(schema, items, ctx) {
500
510
  if (schema.min != null && items.length < schema.min) {
501
511
  _encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
502
512
  }
513
+ if (schema.max != null && items.length > schema.max) {
514
+ _encFail("this repeat caps at " + schema.max + " element(s) but got " + items.length);
515
+ }
503
516
  var parts = items.map(function (it) { return encode(schema.item, it, ctx); });
504
517
  if (schema.unique) {
505
518
  var seen = new Set();
@@ -523,6 +536,96 @@ function _encodeChoice(schema, value, ctx) {
523
536
  return encode(schema.alts[value.arm].schema, value.value, ctx);
524
537
  }
525
538
 
539
+ /**
540
+ * @primitive pki.schema.engine.embeddedDer
541
+ * @signature pki.schema.engine.embeddedDer(schema, bytes, ctx, opts?) -> value
542
+ * @since 0.1.18
543
+ * @status experimental
544
+ * @spec X.690
545
+ * @defends ASN.1-parser-DoS (CWE-400)
546
+ * @related pki.schema.engine.walk, pki.asn1.decode
547
+ *
548
+ * Decode a fresh DER (or, with `ber: true`, BER) blob carried inside an
549
+ * already-decoded value — an OCTET STRING whose content is itself an encoded
550
+ * structure — and walk it against a schema. A codec failure is wrapped in the
551
+ * caller's typed `code`; a schema rejection keeps its own code. This is the
552
+ * one named form of the re-decode idiom, so the caps that a fresh
553
+ * `pki.asn1.decode` would restart from zero can be carried across re-decode
554
+ * boundaries: a shared `budget` (`{ remaining: n }`) decrements on every call
555
+ * and fails with `budgetCode` at zero, bounding how many nested blobs one
556
+ * parse may unwrap however deeply a container chains them.
557
+ *
558
+ * @opts
559
+ * code: string, // typed code wrapping a codec failure (required)
560
+ * what: string, // human label for the wrapped message
561
+ * ber: boolean, // default false — BER content region (RFC 7292 §4.1)
562
+ * budget: object, // { remaining: n } shared across a parse's re-decodes
563
+ * budgetCode: string, // typed code when the budget is exhausted
564
+ *
565
+ * @example
566
+ * var S = pki.schema.engine;
567
+ * var INNER = S.seq([S.field("version", S.integerLeaf())], { code: "app/bad-inner" });
568
+ * var ns = { prefix: "app", E: MyError, oid: pki.oid };
569
+ * S.embeddedDer(INNER, pki.asn1.build.sequence([pki.asn1.build.integer(3n)]), ns,
570
+ * { code: "app/bad-der", what: "the embedded structure" });
571
+ */
572
+ function embeddedDer(schema, bytes, ctx, opts) {
573
+ opts = opts || {};
574
+ if (opts.budget) {
575
+ if (!(opts.budget.remaining > 0)) {
576
+ throw ctx.E(opts.budgetCode || opts.code, (opts.what || "embedded DER") +
577
+ ": the cross-decode budget is exhausted (nesting chained across too many re-decode boundaries)");
578
+ }
579
+ opts.budget.remaining -= 1;
580
+ }
581
+ var node;
582
+ try {
583
+ node = asn1.decode(bytes, opts.ber ? { ber: true } : undefined);
584
+ } catch (e) {
585
+ throw ctx.E(opts.code, (opts.what || "embedded DER") + " did not decode: " + e.message, e);
586
+ }
587
+ return walk(schema, node, ctx);
588
+ }
589
+
590
+ // X.690 §11.2.2 — a NamedBitList BIT STRING (KeyUsage, PKIFailureInfo, and the
591
+ // like) MUST drop every trailing zero bit under DER, giving one canonical
592
+ // encoding per value: an empty value carries 0 unused bits; a non-empty value's
593
+ // last content octet is non-zero AND its lowest USED bit (at position
594
+ // `unusedBits`) is set. `fail(message)` throws the caller's typed domain error,
595
+ // so the reject code stays per-format. Centralized so no format re-derives — and
596
+ // silently diverges on — the minimal-encoding rule (a DER-canonicalization
597
+ // bypass class); see the named-bitlist-minimal-encoding-inlined gate.
598
+ // ASN.1 node tag predicates — the per-node `tagClass`/`tagNumber` test every
599
+ // format's matches() detector (and the engine's own optional/choice walkers)
600
+ // would otherwise re-inline. Centralized so a detector composes one predicate
601
+ // instead of hand-rolling the comparison (and drifting); see the
602
+ // detector-reinlines-root-tag-guard gate.
603
+ function isUniversal(node, tagNumber) {
604
+ return !!node && node.tagClass === "universal" && node.tagNumber === tagNumber;
605
+ }
606
+ function isContext(node, tagNumber) {
607
+ return !!node && node.tagClass === "context" && node.tagNumber === tagNumber;
608
+ }
609
+ function isUniversalOneOf(node, tagNumbers) {
610
+ return !!node && node.tagClass === "universal" && tagNumbers.indexOf(node.tagNumber) !== -1;
611
+ }
612
+ function isContextOneOf(node, tagNumbers) {
613
+ return !!node && node.tagClass === "context" && tagNumbers.indexOf(node.tagNumber) !== -1;
614
+ }
615
+ function isContextInRange(node, min, max) {
616
+ return !!node && node.tagClass === "context" && node.tagNumber >= min && node.tagNumber <= max;
617
+ }
618
+
619
+ function assertMinimalNamedBits(unusedBits, bytes, fail) {
620
+ if (bytes.length === 0) {
621
+ if (unusedBits !== 0) fail("an empty NamedBitList must encode with 0 unused bits (X.690 §11.2.2)");
622
+ return;
623
+ }
624
+ var last = bytes[bytes.length - 1];
625
+ if (last === 0) fail("a NamedBitList must not carry a trailing all-zero octet (X.690 §11.2.2)");
626
+ if (((last >> unusedBits) & 1) !== 1) fail("a NamedBitList must drop all trailing zero bits (X.690 §11.2.2)");
627
+ }
628
+
526
629
  module.exports = {
527
630
  // structural
528
631
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
@@ -533,5 +636,8 @@ module.exports = {
533
636
  implicitNull: implicitNull, implicitInteger: implicitInteger,
534
637
  any: any, decode: decode, time: time,
535
638
  // engine
536
- walk: walk, encode: encode,
639
+ walk: walk, encode: encode, embeddedDer: embeddedDer, assertMinimalNamedBits: assertMinimalNamedBits,
640
+ // node tag predicates
641
+ isUniversal: isUniversal, isContext: isContext,
642
+ isUniversalOneOf: isUniversalOneOf, isContextOneOf: isContextOneOf, isContextInRange: isContextInRange,
537
643
  };
@@ -294,14 +294,12 @@ var RESPONSE_BYTES = schema.seq([
294
294
  if (responseType !== OID_OCSP_BASIC) {
295
295
  throw NS.E("ocsp/unsupported-response-type", (ctx.oid.name(responseType) || responseType) + " is not id-pkix-ocsp-basic (RFC 6960 §4.2.1)");
296
296
  }
297
- var inner;
298
- try { inner = asn1.decode(raw); }
299
- catch (e) { throw NS.E("ocsp/bad-der", "BasicOCSPResponse DER did not decode: " + ((e && e.message) || String(e)), e); }
300
297
  return {
301
298
  responseType: responseType,
302
299
  responseTypeName: ctx.oid.name(responseType) || null,
303
300
  response: raw,
304
- basicResponse: schema.walk(BASIC_OCSP_RESPONSE, inner, ctx).result,
301
+ basicResponse: schema.embeddedDer(BASIC_OCSP_RESPONSE, raw, ctx,
302
+ { code: "ocsp/bad-der", what: "BasicOCSPResponse" }).result,
305
303
  };
306
304
  },
307
305
  });
@@ -491,11 +489,10 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "OCSP RES
491
489
  // unconditionally exclusive.
492
490
  function matchesResponse(root) {
493
491
  var T = asn1.TAGS;
494
- if (!root || root.tagClass !== "universal" || root.tagNumber !== T.SEQUENCE || !root.children) return false;
495
- var k = root.children;
496
- if (k.length < 1 || k.length > 2) return false;
497
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === T.ENUMERATED)) return false;
498
- if (k.length === 2 && !(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
492
+ var k = pkix.rootSequenceChildren(root, 1, 2);
493
+ if (!k) return false;
494
+ if (!schema.isUniversal(k[0], T.ENUMERATED)) return false;
495
+ if (k.length === 2 && !(schema.isContext(k[1], 0) && k[1].children)) return false;
499
496
  return true;
500
497
  }
501
498
 
@@ -505,11 +502,10 @@ function matchesResponse(root) {
505
502
  // signed-envelope trio), while an OCSPRequest is 1-2 — so it is excluded by arity.
506
503
  function matchesRequest(root) {
507
504
  var T = asn1.TAGS;
508
- if (!root || root.tagClass !== "universal" || root.tagNumber !== T.SEQUENCE || !root.children) return false;
509
- var k = root.children;
510
- if (k.length < 1 || k.length > 2) return false;
511
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === T.SEQUENCE && k[0].children)) return false;
512
- if (k.length === 2 && !(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
505
+ var k = pkix.rootSequenceChildren(root, 1, 2);
506
+ if (!k) return false;
507
+ if (!(schema.isUniversal(k[0], T.SEQUENCE) && k[0].children)) return false;
508
+ if (k.length === 2 && !(schema.isContext(k[1], 0) && k[1].children)) return false;
513
509
  return true;
514
510
  }
515
511