@blamejs/pki 0.3.25 → 0.3.26

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/est.js CHANGED
@@ -697,9 +697,30 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
697
697
  var redirects = 0;
698
698
  var authTried = false;
699
699
  var initialOrigin = url.origin; // the origin the caller intended to authenticate to
700
- var tls = budgets.tls; // narrowed (mTLS client identity stripped) on a cross-origin hop
700
+ // The per-hop TLS: the origin-specific identity (mTLS cert/key + pinned SNI) is sent ONLY to the caller's
701
+ // configured origin; a cross-origin hop gets a narrowed copy with those stripped (checkServerIdentity kept).
702
+ // Derived FRESH from budgets.tls each hop (the ACME _tlsFor model) so a redirect back to the original origin
703
+ // restores its identity rather than permanently losing the SNI at the first cross-origin boundary.
704
+ function _tlsFor(u) {
705
+ var t = budgets.tls;
706
+ if (t && u.origin !== initialOrigin && (t.cert != null || t.key != null || t.servername != null)) {
707
+ t = Object.assign({}, t);
708
+ delete t.cert; delete t.key; delete t.servername;
709
+ }
710
+ return t;
711
+ }
712
+ // The per-hop Authorization: the HTTP Basic credential (established after a 401 on the caller's origin) is
713
+ // sent ONLY to that origin. A cross-origin hop travels unauthenticated, but a redirect back to the origin
714
+ // restores it -- so an authenticated flow that bounces off-origin and returns still completes (mirrors _tlsFor).
715
+ var authValue = null;
716
+ function _headersFor(u) {
717
+ if (!authValue || u.origin !== initialOrigin) return headers;
718
+ var hh = Object.assign({}, headers);
719
+ hh.authorization = authValue;
720
+ return hh;
721
+ }
701
722
  function step() {
702
- return transport({ method: method, url: url.href, headers: headers, body: body, tls: tls, timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
723
+ return transport({ method: method, url: url.href, headers: _headersFor(url), body: body, tls: _tlsFor(url), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
703
724
  res = res || {};
704
725
  // Measure an injected string body as UTF-8 -- the byte width the body is decoded/transfer-decoded at,
705
726
  // and what the real socket transport counts -- so a non-ASCII body (multi-byte chars are ~half the
@@ -722,16 +743,10 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
722
743
  body = null;
723
744
  if (headers["content-type"]) { headers = Object.assign({}, headers); delete headers["content-type"]; }
724
745
  }
725
- var prevOrigin = url.origin;
726
746
  url = _redirectTarget(url, h.location, method, opts);
727
- // Credentials MUST NOT cross an origin boundary. A redirect to a different web origin (even a
728
- // permitted cross-origin GET) drops BOTH the HTTP Basic Authorization header AND the mTLS
729
- // client identity (cert/key), so a prior 401's password and the caller's certificate + proof
730
- // of private-key possession are never presented to a different server.
731
- if (url.origin !== prevOrigin) {
732
- if (headers.authorization) { headers = Object.assign({}, headers); delete headers.authorization; }
733
- if (tls && (tls.cert || tls.key)) { tls = Object.assign({}, tls); delete tls.cert; delete tls.key; }
734
- }
747
+ // Credentials MUST NOT cross an origin boundary: the HTTP Basic Authorization header and the
748
+ // ORIGIN-SPECIFIC TLS identity (mTLS cert/key + pinned SNI) are BOTH scoped per hop (_headersFor /
749
+ // _tlsFor), so each is absent off-origin but restored on a hop back to the caller's configured origin.
735
750
  redirects += 1;
736
751
  return step();
737
752
  }
@@ -744,7 +759,9 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
744
759
  var www = String(h["www-authenticate"] || "");
745
760
  if (!_hasBasicChallenge(www)) throw E("est/auth-required", "the server requires an unsupported HTTP authentication scheme (only Basic is supported): " + www);
746
761
  if (opts.username === undefined && opts.password === undefined) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
747
- headers = Object.assign({}, headers, { authorization: "Basic " + Buffer.from((opts.username || "") + ":" + (opts.password || ""), "utf8").toString("base64") });
762
+ // Establish the credential as origin-scoped state (_headersFor attaches it only on the initial origin),
763
+ // never a mutation of the shared headers that would leak across a cross-origin redirect.
764
+ authValue = "Basic " + Buffer.from((opts.username || "") + ":" + (opts.password || ""), "utf8").toString("base64");
748
765
  authTried = true;
749
766
  return step();
750
767
  }
package/lib/lint.js CHANGED
@@ -191,32 +191,9 @@ function _serialOctets(cert) {
191
191
  return buf.length;
192
192
  }
193
193
 
194
- // dNSName syntax (a representative CABF check): no whitespace, no leading/trailing dot,
195
- // no empty label, and no underscore (forbidden in a dNSName). IDN must already be an
196
- // A-label (we do not transcode). Returns a reason string, or null when well-formed.
197
- function _dnsNameProblem(s) {
198
- if (typeof s !== "string" || !s.length) return "empty";
199
- if (s.length > 253) return "exceeds 253 octets";
200
- if (/\s/.test(s)) return "whitespace";
201
- if (s.charAt(0) === "." || s.charAt(s.length - 1) === ".") return "leading/trailing dot";
202
- if (s.indexOf("_") !== -1) return "underscore forbidden in dNSName";
203
- var labels = s.split(".");
204
- for (var i = 0; i < labels.length; i++) {
205
- var label = labels[i];
206
- if (label.length === 0) return "empty label";
207
- if (label.length > 63) return "label exceeds 63 octets";
208
- // A leftmost "*" wildcard label is permitted only when at least one more label follows
209
- // (a bare "*" is not a domain name).
210
- if (i === 0 && label === "*") {
211
- if (labels.length < 2) return "bare wildcard";
212
- continue;
213
- }
214
- // RFC 1034 preferred name syntax: an LDH label that neither begins nor ends with a
215
- // hyphen. Rejects "-bad" / "bad-" and any non-letter/digit/hyphen character.
216
- if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(label)) return "invalid label syntax";
217
- }
218
- return null;
219
- }
194
+ // dNSName syntax check -- the shared RFC 5280 sec. 4.2.1.6 / RFC 1034 preferred-name validator (pkix), reused
195
+ // so the linter and the identity comparators agree on what a well-formed dNSName is. Returns a reason or null.
196
+ function _dnsNameProblem(s) { return pkix.dnsNameProblem(s); }
220
197
 
221
198
  // A genuine IPv4 or IPv6 literal -- a CN validated against an iPAddress SAN rather than a
222
199
  // dNSName. Routed through the shared strict validator (no node:net, so the toolkit needs no
@@ -40,6 +40,7 @@ var crl = require("./schema-crl");
40
40
  var ocsp = require("./schema-ocsp");
41
41
  var ocspVerify = require("./ocsp-verify");
42
42
  var crlVerify = require("./crl-verify");
43
+ var cmpVerify = require("./cmp-verify");
43
44
  var guard = require("./guard-all");
44
45
  var constants = require("./constants");
45
46
  var validator = require("./validator-all");
@@ -1880,6 +1881,13 @@ var ocspCore = ocspVerify.makeOcspVerify({
1880
1881
  dnEqual: dnEqual,
1881
1882
  });
1882
1883
 
1884
+ // Inject this validator's signature engine + full path build/validate into the internal cmp-verify seam,
1885
+ // so pki.cmp.verify routes incoming CMP signature protection through the SAME engine (never a second, weaker
1886
+ // CMP verifier; never build's self-check, which skips the EdDSA low-order-point gate) and chains an
1887
+ // out-of-path signer certificate through the FULL RFC 5280 sec. 6.1 path validation -- without exposing any
1888
+ // of it on the public pki.path surface (index.js exports the whole path-validate module).
1889
+ cmpVerify.setEngine({ verifyWithSpki: _verifyWithSpki, build: build, validate: validate });
1890
+
1883
1891
  /**
1884
1892
  * @primitive pki.path.ocspChecker
1885
1893
  * @signature pki.path.ocspChecker(responses) -> RevocationChecker
@@ -73,11 +73,6 @@ var OID_X509_CRL = oid.byName("x509CRL");
73
73
  var OID_FRIENDLY_NAME = oid.byName("friendlyName");
74
74
  var OID_LOCAL_KEY_ID = oid.byName("localKeyId");
75
75
  var OID_PBMAC1 = oid.byName("pbmac1");
76
- // The PBKDF2-params prf DEFAULT is algid-hmacWithSHA1 -- hmacWithSHA1 with NULL
77
- // parameters (RFC 8018 sec. 5.2) -- needed for the encoded-DEFAULT rejection below.
78
- var OID_HMAC_SHA1 = oid.byName("hmacWithSHA1");
79
- var DER_NULL = asn1.build.nullValue();
80
-
81
76
  // DigestInfo ::= SEQUENCE { digestAlgorithm AlgorithmIdentifier, digest OCTET STRING }.
82
77
  var DIGEST_INFO = schema.seq([
83
78
  schema.field("digestAlgorithm", pkix.algorithmIdentifier(NS)),
@@ -89,67 +84,11 @@ var DIGEST_INFO = schema.seq([
89
84
  },
90
85
  });
91
86
 
92
- // PBKDF2-params (RFC 8018 sec. 5.2), constrained to the RFC 9579 PBMAC1 profile:
93
- // the salt MUST use the specified (OCTET STRING) choice and keyLength MUST be
94
- // present (sec. 4.b / sec. 5 -- a MacData consumer cannot infer the MAC key size).
95
- var PBKDF2_PARAMS = schema.seq([
96
- schema.field("salt", schema.octetString()),
97
- schema.field("iterationCount", schema.integerLeaf()),
98
- schema.optional("keyLength", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
99
- schema.optional("prf", pkix.algorithmIdentifier(NS), { whenUniversal: [TAGS.SEQUENCE] }),
100
- ], {
101
- assert: "sequence", code: "pkcs12/bad-mac-data", what: "PBKDF2-params",
102
- build: function (m, ctx) {
103
- if (!m.fields.keyLength.present) {
104
- throw ctx.E("pkcs12/bad-mac-data", "PBMAC1 PBKDF2-params must carry keyLength (RFC 9579 sec. 5)");
105
- }
106
- // guard.range.positiveInt31 bounds + narrows each counter atomically -- a
107
- // value past the bound would round silently and hand a verifier wrong inputs.
108
- var iterationCount = guard.range.positiveInt31(m.fields.iterationCount.value, ctx.E, "pkcs12/bad-mac-data", "PBKDF2 iterationCount");
109
- var keyLength = guard.range.positiveInt31(m.fields.keyLength.value, ctx.E, "pkcs12/bad-mac-data", "PBKDF2 keyLength");
110
- var prf = m.fields.prf.present ? m.fields.prf.value.result : null;
111
- // X.690 sec. 11.5 -- a DEFAULT-valued component must be omitted from a DER
112
- // encoding, and the prf DEFAULT is algid-hmacWithSHA1: hmacWithSHA1 with
113
- // NULL parameters (RFC 8018 sec. 5.2). An explicit prf byte-equal to that
114
- // default is non-canonical and rejects -- the structured-value analogue of
115
- // the MacData iterations rule below. hmacWithSHA1 with ABSENT parameters
116
- // is a different value from the NULL-parameters default and decodes.
117
- if (prf && prf.oid === OID_HMAC_SHA1 && prf.parameters !== null && prf.parameters.equals(DER_NULL)) {
118
- throw ctx.E("pkcs12/bad-mac-data", "a PBKDF2 prf equal to its DEFAULT algid-hmacWithSHA1 must be omitted (X.690 sec. 11.5, RFC 8018 sec. 5.2)");
119
- }
120
- return {
121
- salt: m.fields.salt.value,
122
- iterationCount: iterationCount,
123
- keyLength: keyLength,
124
- prfOid: prf ? prf.oid : OID_HMAC_SHA1,
125
- prfName: prf ? prf.name : "hmacWithSHA1",
126
- };
127
- },
128
- });
129
-
130
- // PBMAC1-params ::= SEQUENCE { keyDerivationFunc AlgorithmIdentifier{PBKDF2},
131
- // messageAuthScheme AlgorithmIdentifier } (RFC 8018 sec. A.5 / RFC 9579 sec. 4).
132
- var PBMAC1_PARAMS = schema.seq([
133
- schema.field("keyDerivationFunc", schema.seq([
134
- schema.field("algorithm", schema.oidLeaf()),
135
- schema.field("parameters", PBKDF2_PARAMS),
136
- ], { assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-mac-data", what: "PBMAC1 keyDerivationFunc" })),
137
- schema.field("messageAuthScheme", pkix.algorithmIdentifier(NS)),
138
- ], {
139
- assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-mac-data", what: "PBMAC1-params",
140
- build: function (m, ctx) {
141
- var kdf = m.fields.keyDerivationFunc.value;
142
- if (kdf.fields.algorithm.value !== oid.byName("pbkdf2")) {
143
- throw ctx.E("pkcs12/bad-mac-data", "PBMAC1 keyDerivationFunc must be PBKDF2 (RFC 9579 sec. 4)");
144
- }
145
- var scheme = m.fields.messageAuthScheme.value.result;
146
- return {
147
- kdf: kdf.fields.parameters.value.result,
148
- schemeOid: scheme.oid,
149
- schemeName: scheme.name,
150
- };
151
- },
152
- });
87
+ // PBKDF2-params + PBMAC1-params (RFC 8018 sec. 5.2 / App. A.5, RFC 9579 sec. 4) constrained to the PBMAC1
88
+ // profile -- keyLength MUST be present, the prf DEFAULT is enforced non-canonical. Shared with CMP PBMAC1
89
+ // protection verification, so the reader lives once in schema-pkix (ns-parameterized) rather than a second
90
+ // copy per format; composed here with the pkcs12 namespace so it emits pkcs12/bad-mac-data.
91
+ var PBMAC1_PARAMS = pkix.pbmac1Params(NS);
153
92
 
154
93
  // MacData ::= SEQUENCE { mac DigestInfo, macSalt OCTET STRING,
155
94
  // iterations INTEGER DEFAULT 1 }.
@@ -221,6 +221,96 @@ function algorithmIdentifier(ns, opts) {
221
221
  });
222
222
  }
223
223
 
224
+ // pbkdf2Params(ns): PBKDF2-params (RFC 8018 sec. 5.2) constrained to the RFC 9579 PBMAC1 profile -- the
225
+ // salt uses the OCTET STRING choice and keyLength MUST be present (a MacData / PKIProtection consumer
226
+ // cannot infer the derived MAC key size, RFC 9579 sec. 4.b/5). Shared by the PKCS#12 MacData reader and
227
+ // the CMP PBMAC1 protection reader: one ns-parameterized decoder so neither format re-derives the shape.
228
+ function pbkdf2Params(ns) {
229
+ return schema.seq([
230
+ schema.field("salt", schema.octetString()),
231
+ schema.field("iterationCount", schema.integerLeaf()),
232
+ schema.optional("keyLength", schema.integerLeaf(), { whenUniversal: [asn1.TAGS.INTEGER] }),
233
+ schema.optional("prf", algorithmIdentifier(ns), { whenUniversal: [asn1.TAGS.SEQUENCE] }),
234
+ ], {
235
+ assert: "sequence", code: ns.prefix + "/bad-mac-data", what: "PBKDF2-params",
236
+ build: function (m, ctx) {
237
+ var hmacSha1 = ctx.oid.byName("hmacWithSHA1");
238
+ if (!m.fields.keyLength.present) {
239
+ throw ctx.E(ctx.prefix + "/bad-mac-data", "PBMAC1 PBKDF2-params must carry keyLength (RFC 9579 sec. 5)");
240
+ }
241
+ // guard.range.positiveInt31 bounds + narrows each counter atomically -- a value past the bound
242
+ // would round silently and hand a verifier wrong inputs.
243
+ var iterationCount = guard.range.positiveInt31(m.fields.iterationCount.value, ctx.E, ctx.prefix + "/bad-mac-data", "PBKDF2 iterationCount");
244
+ var keyLength = guard.range.positiveInt31(m.fields.keyLength.value, ctx.E, ctx.prefix + "/bad-mac-data", "PBKDF2 keyLength");
245
+ var prf = m.fields.prf.present ? m.fields.prf.value.result : null;
246
+ // X.690 sec. 11.5 -- the prf DEFAULT is algid-hmacWithSHA1 (hmacWithSHA1 with NULL parameters,
247
+ // i.e. the 2-byte DER 05 00, RFC 8018 sec. 5.2); an explicit prf byte-equal to that default is
248
+ // non-canonical and rejects. hmacWithSHA1 with ABSENT parameters is a different value and decodes.
249
+ // This is a public structural check on the algorithm parameters -- not a secret compare -- so a
250
+ // direct byte test on the (fixed 2-octet) NULL encoding, not a timing-safe comparison.
251
+ var pp = prf ? prf.parameters : null;
252
+ // nosemgrep: pki-non-constant-time-secret-compare -- pp is the PUBLIC algorithm-identifier parameters
253
+ // field (a fixed 2-octet NULL encoding), not a MAC / tag / secret; a timing-safe compare is inapplicable.
254
+ if (prf && prf.oid === hmacSha1 && pp !== null && pp.length === 2 && pp[0] === 0x05 && pp[1] === 0x00) {
255
+ throw ctx.E(ctx.prefix + "/bad-mac-data", "a PBKDF2 prf equal to its DEFAULT algid-hmacWithSHA1 must be omitted (X.690 sec. 11.5, RFC 8018 sec. 5.2)");
256
+ }
257
+ // The PBKDF2 prf HMAC AlgorithmIdentifier likewise carries NULL (or absent) parameters (RFC 8018 App. B.1):
258
+ // reject a present-but-non-NULL prf parameter (e.g. an INTEGER) instead of discarding it, since the prf
259
+ // hash is dispatched by OID alone. (An absent prf, and hmacWithSHA1 with absent params, remain valid above.)
260
+ // nosemgrep: pki-non-constant-time-secret-compare -- pp is the PUBLIC algorithm-identifier parameters node.
261
+ if (prf && pp !== null && !(pp.length === 2 && pp[0] === 0x05 && pp[1] === 0x00)) {
262
+ throw ctx.E(ctx.prefix + "/bad-mac-data", "the PBKDF2 prf parameters must be absent or NULL (RFC 8018 App. B.1)");
263
+ }
264
+ return {
265
+ salt: m.fields.salt.value,
266
+ iterationCount: iterationCount,
267
+ keyLength: keyLength,
268
+ prfOid: prf ? prf.oid : hmacSha1,
269
+ prfName: prf ? prf.name : "hmacWithSHA1",
270
+ };
271
+ },
272
+ });
273
+ }
274
+
275
+ // pbmac1Params(ns): PBMAC1-params ::= SEQUENCE { keyDerivationFunc AlgorithmIdentifier{PBKDF2},
276
+ // messageAuthScheme AlgorithmIdentifier } (RFC 8018 App. A.5 / RFC 9579 sec. 4). The PBKDF2 prf and the
277
+ // messageAuthScheme HMAC are INDEPENDENT (a SHA-512 prf with a SHA-256 HMAC is legal). Shared by PKCS#12
278
+ // MacData and CMP PBMAC1 protection; returns { kdf: { salt, iterationCount, keyLength, prfOid, prfName },
279
+ // schemeOid, schemeName }.
280
+ function pbmac1Params(ns) {
281
+ return schema.seq([
282
+ schema.field("keyDerivationFunc", schema.seq([
283
+ schema.field("algorithm", schema.oidLeaf()),
284
+ schema.field("parameters", pbkdf2Params(ns)),
285
+ ], { assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-mac-data", what: "PBMAC1 keyDerivationFunc" })),
286
+ schema.field("messageAuthScheme", algorithmIdentifier(ns)),
287
+ ], {
288
+ assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-mac-data", what: "PBMAC1-params",
289
+ build: function (m, ctx) {
290
+ var kdf = m.fields.keyDerivationFunc.value;
291
+ if (kdf.fields.algorithm.value !== ctx.oid.byName("pbkdf2")) {
292
+ throw ctx.E(ctx.prefix + "/bad-mac-data", "PBMAC1 keyDerivationFunc must be PBKDF2 (RFC 9579 sec. 4)");
293
+ }
294
+ var scheme = m.fields.messageAuthScheme.value.result;
295
+ // RFC 8018 App. B.1: an HMAC messageAuthScheme AlgorithmIdentifier carries NULL (or absent) parameters,
296
+ // matching the builder (_hmacAlgId emits the 2-byte 05 00). Reject any OTHER parameter encoding rather than
297
+ // silently discarding it -- _verifyMac dispatches by the scheme OID alone, so a mismatched / malformed
298
+ // parameter (e.g. an INTEGER) must not slip through unvalidated. A direct byte test on the fixed 2-octet
299
+ // NULL encoding of the PUBLIC parameters, not a secret compare.
300
+ var sp = scheme.parameters;
301
+ // nosemgrep: pki-non-constant-time-secret-compare -- sp is the PUBLIC algorithm-identifier parameters node.
302
+ if (sp !== null && !(sp.length === 2 && sp[0] === 0x05 && sp[1] === 0x00)) {
303
+ throw ctx.E(ctx.prefix + "/bad-mac-data", "the PBMAC1 messageAuthScheme parameters must be absent or NULL (RFC 8018 App. B.1)");
304
+ }
305
+ return {
306
+ kdf: kdf.fields.parameters.value.result,
307
+ schemeOid: scheme.oid,
308
+ schemeName: scheme.name,
309
+ };
310
+ },
311
+ });
312
+ }
313
+
224
314
  // attrValueToString(ns): the AttributeValue decode-leaf. A malformed KNOWN
225
315
  // string type (invalid UTF-8, a non-IA5 byte, a PrintableString character
226
316
  // outside its set, ...) surfaces as an asn1/bad-* content error and must fail
@@ -1319,7 +1409,32 @@ function signedEnvelope(ns, tbsSchema, opts) {
1319
1409
  });
1320
1410
  }
1321
1411
 
1412
+ // dNSName syntax (RFC 5280 sec. 4.2.1.6 / RFC 1034 preferred name syntax, a representative CABF check): no
1413
+ // whitespace, no leading/trailing dot, no empty label, no underscore, LDH labels (1-63 octets, no leading /
1414
+ // trailing hyphen), <= 253 octets, an optional leftmost "*" wildcard. Returns a reason string, or null when
1415
+ // well-formed. The shared home so the linter AND an identity comparator (case-fold only a valid dNSName) agree.
1416
+ function dnsNameProblem(s) {
1417
+ if (typeof s !== "string" || !s.length) return "empty";
1418
+ if (s.length > 253) return "exceeds 253 octets";
1419
+ if (/\s/.test(s)) return "whitespace";
1420
+ if (s.charAt(0) === "." || s.charAt(s.length - 1) === ".") return "leading/trailing dot";
1421
+ if (s.indexOf("_") !== -1) return "underscore forbidden in dNSName";
1422
+ var labels = s.split(".");
1423
+ for (var i = 0; i < labels.length; i++) {
1424
+ var label = labels[i];
1425
+ if (label.length === 0) return "empty label";
1426
+ if (label.length > 63) return "label exceeds 63 octets";
1427
+ if (i === 0 && label === "*") {
1428
+ if (labels.length < 2) return "bare wildcard";
1429
+ continue;
1430
+ }
1431
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(label)) return "invalid label syntax";
1432
+ }
1433
+ return null;
1434
+ }
1435
+
1322
1436
  module.exports = {
1437
+ dnsNameProblem: dnsNameProblem,
1323
1438
  pemDecode: pemDecode,
1324
1439
  pemDecodeAll: pemDecodeAll,
1325
1440
  pemEncode: pemEncode,
@@ -1331,6 +1446,8 @@ module.exports = {
1331
1446
  DN_SHORT: DN_SHORT,
1332
1447
  time: time,
1333
1448
  algorithmIdentifier: algorithmIdentifier,
1449
+ pbkdf2Params: pbkdf2Params,
1450
+ pbmac1Params: pbmac1Params,
1334
1451
  spki: spki,
1335
1452
  makeParser: makeParser,
1336
1453
  signedEnvelopeTbs: signedEnvelopeTbs,
package/lib/webcrypto.js CHANGED
@@ -516,7 +516,35 @@ function _requireDeriveLength(length, who) {
516
516
  // caller must hold differs by entry point (deriveBits requires "deriveBits",
517
517
  // deriveKey requires "deriveKey"), so each public method checks its own
518
518
  // usage and then routes the actual derivation through here.
519
- function _deriveBitsRaw(alg, key, length) {
519
+ // PBKDF2 on the libuv threadpool (crypto.pbkdf2, NOT pbkdf2Sync): an attacker-controlled iteration count must
520
+ // never block the Node event loop (CWE-400 DoS) -- a network peer can request the maximum iterations without
521
+ // knowing the secret (e.g. an unauthenticated PBMAC1 message), so the derivation runs off the main thread.
522
+ // Concurrency is CAPPED so many high-iteration jobs cannot monopolize the whole worker pool and starve
523
+ // unrelated DNS / filesystem / crypto work: at least two pool threads are left free (default pool is four),
524
+ // and derivations beyond the cap queue and run as slots free. The cap tracks UV_THREADPOOL_SIZE when raised.
525
+ var _PBKDF2_MAX_CONCURRENT = Math.max(1, (parseInt(process.env.UV_THREADPOOL_SIZE, 10) || 4) - 2);
526
+ var _pbkdf2InFlight = 0;
527
+ var _pbkdf2Waiters = [];
528
+ function _pbkdf2Async(pw, salt, iterations, keylen, digest) {
529
+ return new Promise(function (resolve, reject) {
530
+ function start() {
531
+ _pbkdf2InFlight++;
532
+ function done(err, derived) { // release the slot + admit the next waiter, THEN settle
533
+ _pbkdf2InFlight--;
534
+ var next = _pbkdf2Waiters.shift();
535
+ if (next) next();
536
+ if (err) reject(err); else resolve(derived);
537
+ }
538
+ // A SYNCHRONOUS argument fault (e.g. iterations 0) never reaches the async callback, so release the slot
539
+ // in the catch too -- otherwise a leaked slot would permanently shrink the pool and queue every later job.
540
+ try { nodeCrypto.pbkdf2(pw, salt, iterations, keylen, digest, done); }
541
+ catch (e) { done(e); }
542
+ }
543
+ if (_pbkdf2InFlight < _PBKDF2_MAX_CONCURRENT) start(); else _pbkdf2Waiters.push(start);
544
+ });
545
+ }
546
+
547
+ async function _deriveBitsRaw(alg, key, length) {
520
548
  var name = alg.name;
521
549
  if (name === "ECDH" || name === "X25519" || name === "X448") {
522
550
  _requireAlgMatch(alg, alg.public, name + " public key");
@@ -538,7 +566,7 @@ function _deriveBitsRaw(alg, key, length) {
538
566
  }
539
567
  if (name === "PBKDF2") {
540
568
  _requireDeriveLength(length, "PBKDF2");
541
- var out = nodeCrypto.pbkdf2Sync(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
569
+ var out = await _pbkdf2Async(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
542
570
  return _toArrayBuffer(out);
543
571
  }
544
572
  if (name === "X963KDF") {
@@ -603,7 +631,7 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
603
631
  // material; a KDF base has no implicit output size and fails closed.
604
632
  bits = dk.length != null ? dk.length : null;
605
633
  }
606
- var raw = _deriveBitsRaw(alg, baseKey, bits);
634
+ var raw = await _deriveBitsRaw(alg, baseKey, bits);
607
635
  return this.importKey("raw", raw, dk, extractable, keyUsages);
608
636
  };
609
637
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
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:5e29cae0-edec-47f9-be9b-305be03dd73c",
5
+ "serialNumber": "urn:uuid:2d9fa8bb-e93e-450a-991c-ef2f3b3fb9e2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-27T02:30:05.287Z",
8
+ "timestamp": "2026-07-30T20:38:00.777Z",
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.3.25",
22
+ "bom-ref": "@blamejs/pki@0.3.26",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.25",
25
+ "version": "0.3.26",
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.3.25",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.26",
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.3.25",
57
+ "ref": "@blamejs/pki@0.3.26",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]