@blamejs/pki 0.8.3 → 0.8.4

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.
@@ -188,7 +188,8 @@ var SAFE_BAG = schema.seq([
188
188
  }
189
189
  if (a.type === OID_LOCAL_KEY_ID) {
190
190
  if (seen[a.type] || a.values.length !== 1) throw ctx.E("pkcs12/bad-local-key-id", "localKeyId carries exactly one OCTET STRING value (PKCS#9 SINGLE VALUE)");
191
- localKeyId = _octetContent(asn1.decode(a.values[0], { ber: true }), ctx, "a localKeyId value", "pkcs12/bad-local-key-id");
191
+ localKeyId = _octetContent(pkix.decodeNested(a.values[0], ctx, "a localKeyId value", { ber: true }), ctx,
192
+ "a localKeyId value", "pkcs12/bad-local-key-id");
192
193
  }
193
194
  seen[a.type] = true;
194
195
  }
@@ -255,7 +256,7 @@ var PFX = schema.seq([
255
256
  }
256
257
  if (authSafe.contentType === OID_SIGNED_DATA) {
257
258
  if (macPresent) throw ctx.E("pkcs12/bad-integrity-mode", "an id-signedData authSafe (public-key integrity) must omit macData (RFC 7292 sec. 4)");
258
- var signed = cms.walkSignedData(authSafe.innerNode);
259
+ var signed = cms.walkSignedData(authSafe.innerNode, ctx);
259
260
  if (signed.encapContentInfo.eContentType !== OID_DATA) {
260
261
  throw ctx.E("pkcs12/bad-authsafe", "a public-key-integrity authSafe must encapsulate id-data carrying the AuthenticatedSafe (RFC 7292 sec. 4.1)");
261
262
  }
@@ -296,7 +297,7 @@ function _privacySafe(content, ctx) {
296
297
  }
297
298
 
298
299
  function _bmpValue(bytes, ctx) {
299
- var node = asn1.decode(bytes, { ber: true });
300
+ var node = pkix.decodeNested(bytes, ctx, "a friendlyName value", { ber: true });
300
301
  if (!(node.tagClass === "universal" && node.tagNumber === TAGS.BMP_STRING)) {
301
302
  throw ctx.E("pkcs12/bad-friendly-name", "a friendlyName value must be a BMPString (RFC 7292 sec. 4.2)");
302
303
  }
@@ -310,12 +311,15 @@ function _walkAuthenticatedSafe(bytes, state, ctx) {
310
311
  });
311
312
  }
312
313
 
313
- // @internal
314
- function walkSafeContents(der) {
315
- var walked = schema.walk(SAFE_CONTENTS, asn1.decode(der, { ber: true }), NS);
314
+ /** @internal Reads a decrypted SafeContents. `ctx` is the parse context of the door that produced
315
+ * the bytes when there is one, so its caps and budget reach the bags; the decrypt path in
316
+ * pki.pkcs12.open has none and takes the built-in ceilings. */
317
+ function walkSafeContents(der, ctx) {
318
+ var ns = schema.withResources(NS, ctx);
319
+ var walked = schema.walk(SAFE_CONTENTS, pkix.decodeNested(der, ns, "a SafeContents", { ber: true }), ns);
316
320
  var state = { budget: { remaining: C.LIMITS.PKCS12_MAX_REDECODES }, bagDepth: 0 };
317
321
  var bags = [];
318
- for (var i = 0; i < walked.items.length; i++) bags.push(_buildBag(walked.items[i].value.result, state, NS));
322
+ for (var i = 0; i < walked.items.length; i++) bags.push(_buildBag(walked.items[i].value.result, state, ns));
319
323
  return bags;
320
324
  }
321
325
 
@@ -333,9 +337,9 @@ function _dispatchSafes(asMatch, state, ctx) {
333
337
  safeBags.push(_buildBag(contents.items[j].value.result, state, ctx));
334
338
  }
335
339
  } else if (el.contentType === OID_ENCRYPTED_DATA) {
336
- encryptedSafes.push({ type: "encryptedData", content: _privacySafe(cms.walkEncryptedData(el.innerNode), ctx) });
340
+ encryptedSafes.push({ type: "encryptedData", content: _privacySafe(cms.walkEncryptedData(el.innerNode, ctx), ctx) });
337
341
  } else if (el.contentType === OID_ENVELOPED_DATA) {
338
- encryptedSafes.push({ type: "envelopedData", content: _privacySafe(cms.walkEnvelopedData(el.innerNode), ctx), envelopedDataDer: el.innerNode.bytes });
342
+ encryptedSafes.push({ type: "envelopedData", content: _privacySafe(cms.walkEnvelopedData(el.innerNode, ctx), ctx), envelopedDataDer: el.innerNode.bytes });
339
343
  } else {
340
344
  throw ctx.E("pkcs12/bad-safe-contentinfo-type", "an AuthenticatedSafe element must be id-data, id-encryptedData, or id-envelopedData (RFC 7292 sec. 4.1), got " +
341
345
  (ctx.oid.name(el.contentType) || el.contentType));
@@ -351,13 +355,13 @@ function _buildBag(rec, state, ctx) {
351
355
  };
352
356
  if (rec.bagId === OID_KEY_BAG) {
353
357
  bag.type = "keyBag";
354
- bag.key = pkcs8.walkPrivateKeyInfo(rec.valueNode);
358
+ bag.key = pkcs8.walkPrivateKeyInfo(rec.valueNode, ctx);
355
359
  bag.keyDer = rec.valueNode.bytes;
356
360
  return bag;
357
361
  }
358
362
  if (rec.bagId === OID_SHROUDED_KEY_BAG) {
359
363
  bag.type = "pkcs8ShroudedKeyBag";
360
- bag.encrypted = pkcs8.walkEncryptedPrivateKeyInfo(rec.valueNode);
364
+ bag.encrypted = pkcs8.walkEncryptedPrivateKeyInfo(rec.valueNode, ctx);
361
365
  return bag;
362
366
  }
363
367
  if (rec.bagId === OID_CERT_BAG) {
@@ -193,8 +193,15 @@ function matches(root) {
193
193
  }
194
194
 
195
195
 
196
- function walkPrivateKeyInfo(node) { return schema.walk(PRIVATE_KEY_INFO, node, NS).result; }
197
- function walkEncryptedPrivateKeyInfo(node) { return schema.walk(ENCRYPTED_PRIVATE_KEY_INFO, node, NS).result; }
196
+ /** @internal `ctx` is the walk context of the parse that reached this key, when another format
197
+ * carries it. Only its caps and budget cross over: the key is still read in this namespace, so a
198
+ * fault in it keeps the PKCS#8 error class and code. */
199
+ function walkPrivateKeyInfo(node, ctx) {
200
+ return schema.walk(PRIVATE_KEY_INFO, node, schema.withResources(NS, ctx)).result;
201
+ }
202
+ function walkEncryptedPrivateKeyInfo(node, ctx) {
203
+ return schema.walk(ENCRYPTED_PRIVATE_KEY_INFO, node, schema.withResources(NS, ctx)).result;
204
+ }
198
205
 
199
206
  module.exports = {
200
207
  parse: parse,
@@ -222,17 +222,16 @@ function _isPemArmor(buf) {
222
222
  /** @internal The codec's resource refusals, as distinct from its malformed-input refusals. A
223
223
  * caller who tightened a cap has to be able to tell "your bytes are wrong" from "your cap is
224
224
  * too small", and only the second is answered by raising the cap. */
225
- var RESOURCE_REFUSALS = intrinsic.assign(intrinsic.create(null), {
226
- "asn1/too-large": 1, "asn1/too-deep": 1, "asn1/too-many-items": 1,
227
- });
225
+ var _isResourceRefusal = schema.isResourceRefusal;
228
226
 
229
227
  function decodeRoot(der, opts, caps, budget) {
228
+ // allow:bare-decode the root decode a door performs, which is where the caller's caps enter.
230
229
  try { return asn1.decode(der, _decodeOptions(opts, caps)); }
231
230
  catch (e) {
232
231
  /** @internal `rawDecodeErrors` keeps the codec's own code for a malformed body, which some
233
232
  * callers pin. A resource refusal is still translated, that code being new. */
234
- if (opts.rawDecodeErrors && !(e && RESOURCE_REFUSALS[e.code])) throw e;
235
- if (e && RESOURCE_REFUSALS[e.code]) {
233
+ if (opts.rawDecodeErrors && !_isResourceRefusal(e)) throw e;
234
+ if (_isResourceRefusal(e)) {
236
235
  if (budget) {
237
236
  try {
238
237
  budget.trip(opts.what || "the input");
@@ -338,12 +337,18 @@ var _emptyOptions = intrinsic.freeze(intrinsic.create(null));
338
337
  function runParseWith(input, opts, callerOpts, build) {
339
338
  var caps = parseCaps(callerOpts, opts);
340
339
  var budget = guard.limits.budget({ maxBytes: caps.maxBytes });
341
- var ns = _nsWithBudget(opts.ns, budget);
340
+ var ns = _nsWithBudget(opts.ns, budget, caps);
342
341
  var result;
343
342
  try {
344
343
  result = build(decodeRoot(coerceToDer(input, opts), opts, caps, budget), ns, caps, budget);
345
344
  } catch (e) {
346
345
  if (guard.limits.isBudgetExceeded(e)) throw _budgetRefusal(opts, e);
346
+ /** @internal A refusal that is already this door's own resource verdict has been translated
347
+ * once and carries the codec's refusal as its cause; wrapping it again would put a copy of
348
+ * the verdict where a caller looks for the cap that was exceeded. */
349
+ if (budget.exhausted() && !(e && e.code === opts.prefix + "/too-large")) {
350
+ throw _budgetRefusal(opts, e);
351
+ }
347
352
  throw e;
348
353
  }
349
354
  if (budget.exhausted()) throw _budgetRefusal(opts, null);
@@ -357,17 +362,31 @@ function runParse(input, opts, callerOpts) {
357
362
  }
358
363
 
359
364
  /** @internal The re-read at the door is what makes the latch worth having: a catch anywhere
360
- * under the walk may absorb the exception, and this still refuses. */
365
+ * under the walk may absorb the exception or rewrap it as a domain error, and this still
366
+ * refuses. The rewrapped error is carried as the cause. */
361
367
  function _budgetRefusal(opts, cause) {
362
368
  return new opts.ErrorClass(opts.prefix + "/too-large",
363
369
  (opts.what || "the input") + " exceeds the decode budget for this parse", cause || undefined);
364
370
  }
365
371
 
366
- function _nsWithBudget(ns, budget) {
367
- var out = intrinsic.create(null);
368
- _forEach(intrinsic.keys(ns), function (k) { out[k] = ns[k]; });
369
- out.budget = budget;
370
- return out;
372
+ function _nsWithBudget(ns, budget, caps) {
373
+ return schema.withResources(ns, { budget: budget, caps: caps });
374
+ }
375
+
376
+ /** @internal The walk context a hand-rolled door builds for the structures it reads out of its
377
+ * own input, so the caller's caps reach them the way `runParseWith` makes them reach a factory
378
+ * parser's. The door re-reads `budget.exhausted()` before it returns a result. */
379
+ function nestedContext(ns, caps) {
380
+ var budget = guard.limits.budget({ maxBytes: caps.maxBytes });
381
+ return { ns: _nsWithBudget(ns, budget, caps), budget: budget };
382
+ }
383
+
384
+ /** @internal The decode a format performs on a value it has already read out of its input: an
385
+ * embedded OCTET STRING payload, an encapsulated protocol body. The engine owns it, because
386
+ * `schema.embeddedDer` performs the same decode from inside a walk and the two must apply the
387
+ * caller's caps and trip the parse budget the same way. This is the name format modules call. */
388
+ function decodeNested(bytes, ctx, label, opts) {
389
+ return schema.decodeNested(bytes, ctx, label, opts);
371
390
  }
372
391
 
373
392
  var DN_SHORT = constants.NAMES.DN_SHORT;
@@ -489,6 +508,7 @@ function attrValueToString(ns) {
489
508
  throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
490
509
  }
491
510
  var raw = guard.encoding.hex(hex, null, ns.E, ns.prefix + "/bad-atv", "a #hex attribute value");
511
+ // allow:bare-decode the encoder side, checking bytes a caller wrote by hand, outside any parse.
492
512
  try { asn1.decode(raw); }
493
513
  catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
494
514
  return raw;
@@ -826,7 +846,7 @@ function decodeIssuingDistributionPointValue(ns, bytes, code) {
826
846
  indirect: false, onlyAttr: false, fields: null };
827
847
  var m;
828
848
  try {
829
- m = schema.walk(issuingDistributionPoint(code), asn1.decode(bytes), ns);
849
+ m = schema.walk(issuingDistributionPoint(code), schema.decodeNested(bytes, ns, "an IssuingDistributionPoint value"), ns);
830
850
  if (m.fields.distributionPoint.present) {
831
851
  var wrap = m.fields.distributionPoint.node;
832
852
  if (!wrap.children || wrap.children.length !== 1) {
@@ -928,15 +948,20 @@ function userNoticeTexts(qualifier) {
928
948
  return i === kids.length ? out : [];
929
949
  }
930
950
 
951
+ /** @internal The readers an extension or attribute value is decoded through. `ctx` is the walk
952
+ * context of the parse that reached the value, when one did: the bytes here are an OCTET STRING
953
+ * payload the root decode stopped at, so this decode restarts depth and item counting and needs
954
+ * the caller's caps handed to it. A caller outside a parse passes none and takes the built-in
955
+ * ceilings, which is what `ns` carries. */
931
956
  function _decodeHelpers(ns) {
932
- function decodeTop(buf, code, what) {
957
+ function decodeTop(buf, code, what, ctx) {
933
958
  var n;
934
- try { n = asn1.decode(buf); }
959
+ try { n = schema.decodeNested(buf, ctx || ns, what); }
935
960
  catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || _String(e)), e); }
936
961
  return n;
937
962
  }
938
- function seqChildren(buf, code, what) {
939
- var n = decodeTop(buf, code, what);
963
+ function seqChildren(buf, code, what, ctx) {
964
+ var n = decodeTop(buf, code, what, ctx);
940
965
  if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
941
966
  throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 sec. 4.2.1)");
942
967
  }
@@ -1576,6 +1601,8 @@ module.exports = {
1576
1601
  isCborResourceRefusal: isCborResourceRefusal,
1577
1602
  cborParseOpts: cborParseOpts,
1578
1603
  parseOpts: parseOpts,
1604
+ decodeNested: decodeNested,
1605
+ nestedContext: nestedContext,
1579
1606
  makeNS: makeNS,
1580
1607
  versionReader: versionReader,
1581
1608
  DN_SHORT: DN_SHORT,
package/lib/schema-tsp.js CHANGED
@@ -204,7 +204,7 @@ var TIME_STAMP_RESP = schema.seq([
204
204
  schema.optional("timeStampToken", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
205
205
  ], {
206
206
  assert: "sequence", arity: { min: 1 }, code: "tsp/bad-response", what: "TimeStampResp",
207
- build: function (m) {
207
+ build: function (m, ctx) {
208
208
  var status = m.fields.status.value.result;
209
209
  var present = m.fields.timeStampToken.present;
210
210
  var granted = status.status === 0 || status.status === 1;
@@ -215,7 +215,9 @@ var TIME_STAMP_RESP = schema.seq([
215
215
  status: status.status,
216
216
  statusString: status.statusString,
217
217
  failInfo: status.failInfo,
218
- timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes) : null,
218
+ /** @internal The token is a second parse of bytes carried inside this one, so it takes the
219
+ * caps this door was given rather than the built-in ceilings. */
220
+ timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes, ctx ? ctx.caps : undefined) : null,
219
221
  };
220
222
  },
221
223
  });
@@ -424,12 +426,23 @@ function parseToken(input, callerOpts) {
424
426
  if (!hasSigningCert) {
425
427
  throw new TspError("tsp/missing-signing-certificate", "a TimeStampToken signerInfo must carry a SigningCertificate or SigningCertificateV2 signed attribute (RFC 3161 sec. 2.4.2, RFC 5816)");
426
428
  }
429
+ /** @internal The TSTInfo is decoded out of the eContent the CMS layer handed back, so it is a
430
+ * second decode and takes the caller's caps the way the carrier did. The latch is re-read
431
+ * below, because the catch here answers in this domain and would otherwise report a cap that
432
+ * was too small as a malformed TSTInfo. */
433
+ var nested = pkix.nestedContext(NS, caps);
427
434
  var tstInfo;
428
- try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent, NS, { code: "tsp/bad-der", what: "the encapsulated TSTInfo" }); }
435
+ try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent, nested.ns, { code: "tsp/bad-der", what: "the encapsulated TSTInfo" }); }
429
436
  catch (e) {
437
+ if (nested.budget.exhausted()) {
438
+ throw new TspError("tsp/too-large", "the timeStampToken exceeds the decode budget for this parse", e);
439
+ }
430
440
  if (e instanceof TspError) throw e;
431
441
  throw new TspError("tsp/bad-der", "the encapsulated TSTInfo did not decode: " + ((e && e.message) || String(e)), e);
432
442
  }
443
+ if (nested.budget.exhausted()) {
444
+ throw new TspError("tsp/too-large", "the timeStampToken exceeds the decode budget for this parse");
445
+ }
433
446
  return { tstInfo: tstInfo.result, eContent: encap.eContent, signerInfo: signed.signerInfos[0], certificates: signed.certificates };
434
447
  }
435
448
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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:9e704e55-ccb2-4ecb-9370-04d6f58aa807",
5
+ "serialNumber": "urn:uuid:ca3c695a-64b2-4346-9529-6a6c719eb6bb",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-20T04:03:01.349Z",
8
+ "timestamp": "2026-09-20T06:26:19.862Z",
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.3",
22
+ "bom-ref": "@blamejs/pki@0.8.4",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.3",
25
+ "version": "0.8.4",
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.3",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.4",
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.3",
57
+ "ref": "@blamejs/pki@0.8.4",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]