@blamejs/pki 0.1.18 → 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
 
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 = {
@@ -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
  }
@@ -587,6 +587,45 @@ function embeddedDer(schema, bytes, ctx, opts) {
587
587
  return walk(schema, node, ctx);
588
588
  }
589
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
+
590
629
  module.exports = {
591
630
  // structural
592
631
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
@@ -597,5 +636,8 @@ module.exports = {
597
636
  implicitNull: implicitNull, implicitInteger: implicitInteger,
598
637
  any: any, decode: decode, time: time,
599
638
  // engine
600
- walk: walk, encode: encode, embeddedDer: embeddedDer,
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,
601
643
  };
@@ -489,11 +489,10 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "OCSP RES
489
489
  // unconditionally exclusive.
490
490
  function matchesResponse(root) {
491
491
  var T = asn1.TAGS;
492
- if (!root || root.tagClass !== "universal" || root.tagNumber !== T.SEQUENCE || !root.children) return false;
493
- var k = root.children;
494
- if (k.length < 1 || k.length > 2) return false;
495
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === T.ENUMERATED)) return false;
496
- 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;
497
496
  return true;
498
497
  }
499
498
 
@@ -503,11 +502,10 @@ function matchesResponse(root) {
503
502
  // signed-envelope trio), while an OCSPRequest is 1-2 — so it is excluded by arity.
504
503
  function matchesRequest(root) {
505
504
  var T = asn1.TAGS;
506
- if (!root || root.tagClass !== "universal" || root.tagNumber !== T.SEQUENCE || !root.children) return false;
507
- var k = root.children;
508
- if (k.length < 1 || k.length > 2) return false;
509
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === T.SEQUENCE && k[0].children)) return false;
510
- 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;
511
509
  return true;
512
510
  }
513
511
 
@@ -595,15 +595,14 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label || "PKCS12", P
595
595
  // a shape a PrivateKeyInfo's AlgorithmIdentifier never presents) and
596
596
  // children[2] (a SEQUENCE MacData or absent, never PKCS#8's OCTET STRING).
597
597
  function matches(root) {
598
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
599
- var k = root.children;
600
- if (!k || k.length < 2 || k.length > 3) return false;
601
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
598
+ var k = pkix.rootSequenceChildren(root, 2, 3);
599
+ if (!k) return false;
600
+ if (!schema.isUniversal(k[0], TAGS.INTEGER)) return false;
602
601
  var ci = k[1];
603
- if (!ci.children || ci.tagClass !== "universal" || ci.tagNumber !== TAGS.SEQUENCE || ci.children.length !== 2) return false;
604
- if (!(ci.children[0].tagClass === "universal" && ci.children[0].tagNumber === TAGS.OBJECT_IDENTIFIER)) return false;
605
- if (!(ci.children[1].tagClass === "context" && ci.children[1].tagNumber === 0 && ci.children[1].children)) return false;
606
- if (k.length === 3 && !(k[2].tagClass === "universal" && k[2].tagNumber === TAGS.SEQUENCE && k[2].children)) return false;
602
+ if (!(ci.children && schema.isUniversal(ci, TAGS.SEQUENCE) && ci.children.length === 2)) return false;
603
+ if (!schema.isUniversal(ci.children[0], TAGS.OBJECT_IDENTIFIER)) return false;
604
+ if (!(schema.isContext(ci.children[1], 0) && ci.children[1].children)) return false;
605
+ if (k.length === 3 && !(schema.isUniversal(k[2], TAGS.SEQUENCE) && k[2].children)) return false;
607
606
  return true;
608
607
  }
609
608
 
@@ -179,15 +179,14 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label || "PRIVATE KE
179
179
  // order.
180
180
  function matches(root) {
181
181
  var TAGS = asn1.TAGS;
182
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
183
- var k = root.children;
184
- if (!k || k.length < 3 || k.length > 5) return false;
185
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
186
- if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
187
- if (!(k[2].tagClass === "universal" && k[2].tagNumber === TAGS.OCTET_STRING)) return false;
182
+ var k = pkix.rootSequenceChildren(root, 3, 5);
183
+ if (!k) return false;
184
+ if (!schema.isUniversal(k[0], TAGS.INTEGER)) return false;
185
+ if (!schema.isUniversal(k[1], TAGS.SEQUENCE)) return false;
186
+ if (!schema.isUniversal(k[2], TAGS.OCTET_STRING)) return false;
188
187
  var last = -1;
189
188
  for (var i = 3; i < k.length; i++) {
190
- if (k[i].tagClass !== "context" || k[i].tagNumber <= last || k[i].tagNumber > 1) return false;
189
+ if (!schema.isContextInRange(k[i], 0, 1) || k[i].tagNumber <= last) return false;
191
190
  last = k[i].tagNumber;
192
191
  }
193
192
  return true;
@@ -392,7 +392,10 @@ function certExtensionDecoders(ns) {
392
392
  if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
393
393
  if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 §4.2.1.9)");
394
394
  var pl = readInt(kids[i], C, "pathLenConstraint");
395
- if (pl < 0n) throw ns.E(C, "BasicConstraints pathLenConstraint must be non-negative");
395
+ // Bounded before Number() narrowing a value past the safe-integer range
396
+ // would round silently and be compared as a path-length ceiling, so it is
397
+ // exact-or-rejected (the same rule the RSASSA-PSS/PKCS#12 counters follow).
398
+ if (pl < 0n || pl > 2147483647n) throw ns.E(C, "BasicConstraints pathLenConstraint must be a non-negative integer within range");
396
399
  pathLen = Number(pl); i++;
397
400
  }
398
401
  if (i !== kids.length) throw ns.E(C, "BasicConstraints has unexpected trailing fields");
@@ -413,13 +416,8 @@ function certExtensionDecoders(ns) {
413
416
  var anyBit = false;
414
417
  for (var z = 0; z < bs.bytes.length; z++) { if (bs.bytes[z] !== 0) { anyBit = true; break; } }
415
418
  if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280 §4.2.1.3)");
416
- // KeyUsage is a NamedBitList: DER (X.690 §11.2.2) strips all trailing zero
417
- // bits, so the last content octet must be non-zero and the lowest USED bit
418
- // (at position unusedBits) must be set — one canonical encoding per value.
419
- var last = bs.bytes[bs.bytes.length - 1];
420
- if (last === 0 || ((last >> bs.unusedBits) & 1) !== 1) {
421
- throw ns.E(C, "KeyUsage must use the minimal DER NamedBitList encoding (no trailing zero bits, X.690 §11.2.2)");
422
- }
419
+ // KeyUsage is a NamedBitList enforce the shared X.690 §11.2.2 minimal-DER rule.
420
+ schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw ns.E(C, m); });
423
421
  var out = {};
424
422
  KU_BITS.forEach(function (nm, bit) {
425
423
  var byte = bit >> 3, mask = 0x80 >> (bit & 7);
@@ -532,7 +530,9 @@ function certExtensionDecoders(ns) {
532
530
  pcLastTag = k.tagNumber;
533
531
  var v;
534
532
  try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
535
- if (v < 0n) throw ns.E(C, "PolicyConstraints skip count must be non-negative");
533
+ // Bounded before Number() narrowing (a skip count past the safe-integer
534
+ // range would round silently) — exact-or-rejected.
535
+ if (v < 0n || v > 2147483647n) throw ns.E(C, "PolicyConstraints skip count must be a non-negative integer within range");
536
536
  if (k.tagNumber === 0) rep = Number(v);
537
537
  else if (k.tagNumber === 1) ipm = Number(v);
538
538
  else throw ns.E(C, "PolicyConstraints has an unexpected field [" + k.tagNumber + "]");
@@ -546,7 +546,9 @@ function certExtensionDecoders(ns) {
546
546
  var n = decodeTop(buf, C, "InhibitAnyPolicy");
547
547
  if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 §4.2.1.14)");
548
548
  var v = readInt(n, C, "InhibitAnyPolicy");
549
- if (v < 0n) throw ns.E(C, "InhibitAnyPolicy skip count must be non-negative");
549
+ // Bounded before Number() narrowing (a skip count past the safe-integer
550
+ // range would round silently) — exact-or-rejected.
551
+ if (v < 0n || v > 2147483647n) throw ns.E(C, "InhibitAnyPolicy skip count must be a non-negative integer within range");
550
552
  return Number(v);
551
553
  }
552
554
 
@@ -725,13 +727,25 @@ function attribute(ns) {
725
727
  // detectors cannot drift on it (the CRL detector historically omitted the
726
728
  // tbs-is-universal check this recovers).
727
729
  function signedEnvelopeTbs(root) {
728
- if (!root || root.tagClass !== "universal" || root.tagNumber !== asn1.TAGS.SEQUENCE) return null;
730
+ if (!schema.isUniversal(root, asn1.TAGS.SEQUENCE)) return null;
729
731
  if (!root.children || root.children.length !== 3) return null;
730
732
  var tbs = root.children[0];
731
- if (!tbs.children || tbs.tagClass !== "universal" || tbs.tagNumber !== asn1.TAGS.SEQUENCE) return null;
733
+ if (!tbs.children || !schema.isUniversal(tbs, asn1.TAGS.SEQUENCE)) return null;
732
734
  return tbs;
733
735
  }
734
736
 
737
+ // rootSequenceChildren(root, minLen, maxLen) — the shared root-shape guard the
738
+ // format matches() detectors re-inline: returns root.children when root is a
739
+ // NON-EMPTY universal SEQUENCE whose child count is within [minLen, maxLen]
740
+ // (maxLen optional/Infinity), else null. Sibling of signedEnvelopeTbs.
741
+ function rootSequenceChildren(root, minLen, maxLen) {
742
+ if (!schema.isUniversal(root, asn1.TAGS.SEQUENCE) || !root.children) return null;
743
+ var n = root.children.length;
744
+ if (n < (minLen || 0)) return null;
745
+ if (maxLen != null && n > maxLen) return null;
746
+ return root.children;
747
+ }
748
+
735
749
  // Every format's `parse` is the shared runParse bound to that format's identity
736
750
  // (PEM label, error class, error-code prefix, top-level schema). This returns the
737
751
  // bound parser so a format declares its configuration once and never re-writes the
@@ -785,6 +799,7 @@ module.exports = {
785
799
  spki: spki,
786
800
  makeParser: makeParser,
787
801
  signedEnvelopeTbs: signedEnvelopeTbs,
802
+ rootSequenceChildren: rootSequenceChildren,
788
803
  signedEnvelope: signedEnvelope,
789
804
  attrValueToString: attrValueToString,
790
805
  attributeTypeAndValue: attributeTypeAndValue,
package/lib/schema-tsp.js CHANGED
@@ -217,13 +217,7 @@ var PKI_STATUS_INFO = schema.seq([
217
217
  // declared unusedBits must sit exactly below the lowest set bit of the last octet (so
218
218
  // the encoding is minimal). The empty value encodes as 0 content octets, 0 unusedBits.
219
219
  function _assertMinimalNamedBits(unusedBits, bytes) {
220
- if (bytes.length === 0) {
221
- if (unusedBits !== 0) throw NS.E("tsp/bad-failinfo", "an empty PKIFailureInfo must encode with 0 unused bits (X.690 §11.2.2)");
222
- return;
223
- }
224
- var last = bytes[bytes.length - 1];
225
- if (last === 0) throw NS.E("tsp/bad-failinfo", "PKIFailureInfo NamedBitList must not have a trailing all-zero octet (X.690 §11.2.2)");
226
- if (((last >> unusedBits) & 1) !== 1) throw NS.E("tsp/bad-failinfo", "PKIFailureInfo NamedBitList must have all trailing zero bits removed (X.690 §11.2.2)");
220
+ schema.assertMinimalNamedBits(unusedBits, bytes, function (m) { throw NS.E("tsp/bad-failinfo", m); });
227
221
  }
228
222
 
229
223
  // Decode the set NamedBitList bits to their RFC 3161 names (bit 0 = MSB of byte 0).
@@ -277,7 +271,7 @@ var TIME_STAMP_RESP = schema.seq([
277
271
  * @primitive pki.schema.tsp.parseTstInfo
278
272
  * @signature pki.schema.tsp.parseTstInfo(input) -> tstInfo
279
273
  * @since 0.1.13
280
- * @status experimental
274
+ * @status stable
281
275
  * @spec RFC 3161
282
276
  * @related pki.schema.tsp.parseToken, pki.schema.tsp.parse
283
277
  *
@@ -299,7 +293,7 @@ var parseTstInfo = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorCl
299
293
  * @primitive pki.schema.tsp.parse
300
294
  * @signature pki.schema.tsp.parse(input) -> timeStampResp
301
295
  * @since 0.1.13
302
- * @status experimental
296
+ * @status stable
303
297
  * @spec RFC 3161
304
298
  * @related pki.schema.tsp.parseToken, pki.schema.parse
305
299
  *
@@ -320,7 +314,7 @@ var parse = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: Ts
320
314
  * @primitive pki.schema.tsp.parseToken
321
315
  * @signature pki.schema.tsp.parseToken(input) -> tstInfo
322
316
  * @since 0.1.13
323
- * @status experimental
317
+ * @status stable
324
318
  * @spec RFC 3161, RFC 5652
325
319
  * @related pki.schema.tsp.parse, pki.schema.cms.parse
326
320
  *
@@ -374,7 +368,7 @@ function parseToken(input) {
374
368
  * @primitive pki.schema.tsp.pemDecode
375
369
  * @signature pki.schema.tsp.pemDecode(text, label?) -> Buffer
376
370
  * @since 0.1.13
377
- * @status experimental
371
+ * @status stable
378
372
  * @spec RFC 7468, RFC 3161
379
373
  * @related pki.schema.tsp.parse
380
374
  *
@@ -392,13 +386,12 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, Pem
392
386
  // CMS ContentInfo, the INTEGER-first PKCS#8 key, and the exactly-3 signed-envelope
393
387
  // trio, so it detects unambiguously regardless of registry order.
394
388
  function matches(root) {
395
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children) return false;
396
- var k = root.children;
397
- if (k.length < 1 || k.length > 2) return false;
389
+ var k = pkix.rootSequenceChildren(root, 1, 2);
390
+ if (!k) return false;
398
391
  var si = k[0];
399
- if (!(si.tagClass === "universal" && si.tagNumber === TAGS.SEQUENCE && si.children && si.children.length >= 1)) return false;
400
- if (!(si.children[0].tagClass === "universal" && si.children[0].tagNumber === TAGS.INTEGER)) return false;
401
- if (k.length === 2 && !(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
392
+ if (!(schema.isUniversal(si, TAGS.SEQUENCE) && si.children && si.children.length >= 1)) return false;
393
+ if (!schema.isUniversal(si.children[0], TAGS.INTEGER)) return false;
394
+ if (k.length === 2 && !schema.isUniversal(k[1], TAGS.SEQUENCE)) return false;
402
395
  return true;
403
396
  }
404
397
 
@@ -220,12 +220,12 @@ function matches(root) {
220
220
  var tbs = pkix.signedEnvelopeTbs(root);
221
221
  if (!tbs) return false;
222
222
  var kids = tbs.children;
223
- var i = (kids[0] && kids[0].tagClass === "context" && kids[0].tagNumber === 0) ? 1 : 0; // optional [0] version
223
+ var i = schema.isContext(kids[0], 0) ? 1 : 0; // optional [0] version
224
224
  var validity = kids[i + 3]; // serial(i), signature(i+1), issuer(i+2), validity(i+3)
225
- return !!validity && validity.tagClass === "universal" && validity.tagNumber === TAGS.SEQUENCE &&
225
+ return schema.isUniversal(validity, TAGS.SEQUENCE) &&
226
226
  !!validity.children && validity.children.length === 2 &&
227
227
  validity.children.every(function (t) {
228
- return t.tagClass === "universal" && (t.tagNumber === TAGS.UTC_TIME || t.tagNumber === TAGS.GENERALIZED_TIME);
228
+ return schema.isUniversalOneOf(t, [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME]);
229
229
  });
230
230
  }
231
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
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:a14a33cf-8c68-40d3-92b0-6f17ef343543",
5
+ "serialNumber": "urn:uuid:c8d0b092-58fa-4019-9da1-b2b636e61608",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-09T03:55:56.855Z",
8
+ "timestamp": "2026-07-09T11:21:47.988Z",
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.1.18",
22
+ "bom-ref": "@blamejs/pki@0.1.19",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.18",
25
+ "version": "0.1.19",
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.1.18",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.19",
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.1.18",
57
+ "ref": "@blamejs/pki@0.1.19",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]