@blamejs/pki 0.3.24 → 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.
@@ -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
@@ -809,6 +899,30 @@ function certExtensionDecoders(ns) {
809
899
  return schema.walk(generalNames(ns, { decodeValue: true, code: C }), n, ns).result;
810
900
  }
811
901
 
902
+ // authorityInfoAccess ::= AuthorityInfoAccessSyntax ::= SEQUENCE SIZE(1..MAX) OF AccessDescription
903
+ // (RFC 5280 sec. 4.2.2.1); AccessDescription ::= SEQUENCE { accessMethod OBJECT IDENTIFIER, accessLocation
904
+ // GeneralName }. Surfaces [{ accessMethod: <dotted OID>, accessLocation: { tag, value } }] in wire order --
905
+ // accessLocation is the shared generalName leaf (its context tag number + decoded value, so a control-byte
906
+ // URI is rejected by the CVE-2009-2408 guard). BOTH accessMethods surface (id-ad-caIssuers AND id-ad-ocsp):
907
+ // a consumer filters by accessMethod (caIssuers for issuer fetching, ocsp for responder discovery). An empty
908
+ // SEQUENCE violates SIZE(1..MAX) and is malformed. A composed decoder + registry row, not a hand-roll.
909
+ function authorityInfoAccess(buf) {
910
+ var C = ns.prefix + "/bad-extension-value";
911
+ var descs = seqChildren(buf, C, "AuthorityInfoAccessSyntax");
912
+ if (descs.length < 1) throw ns.E(C, "AuthorityInfoAccessSyntax must contain at least one AccessDescription (RFC 5280 sec. 4.2.2.1, SIZE(1..MAX))");
913
+ var GN = generalName(ns, { decodeValue: true, code: C });
914
+ return descs.map(function (d) {
915
+ if (d.tagClass !== "universal" || d.tagNumber !== _T.SEQUENCE || !d.children || d.children.length !== 2) {
916
+ throw ns.E(C, "AccessDescription must be a SEQUENCE { accessMethod, accessLocation } (RFC 5280 sec. 4.2.2.1)");
917
+ }
918
+ var method;
919
+ try { method = asn1.read.oid(d.children[0]); }
920
+ catch (e) { throw ns.E(C, "AccessDescription accessMethod must be an OBJECT IDENTIFIER", e); }
921
+ var loc = schema.walk(GN, d.children[1], ns); // a decode leaf returns its value directly (no .result wrapper)
922
+ return { accessMethod: method, accessLocation: { tag: loc.tagNumber, value: loc.value } };
923
+ });
924
+ }
925
+
812
926
  // extKeyUsage ::= SEQUENCE SIZE(1..MAX) OF KeyPurposeId (OID)
813
927
  function extKeyUsage(buf) {
814
928
  var C = ns.prefix + "/bad-extension-value";
@@ -1105,6 +1219,7 @@ function certExtensionDecoders(ns) {
1105
1219
  byOid[O("precertificatePoison")] = precertPoison;
1106
1220
  byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
1107
1221
  byOid[O("freshestCRL")] = crlDistributionPoints;
1222
+ byOid[O("authorityInfoAccess")] = authorityInfoAccess;
1108
1223
  byOid[O("msCertificateTemplate")] = msCertificateTemplate;
1109
1224
  byOid[O("msEnrollCertType")] = msEnrollCertType;
1110
1225
  byOid[O("msCaVersion")] = msCaVersion;
@@ -1294,7 +1409,32 @@ function signedEnvelope(ns, tbsSchema, opts) {
1294
1409
  });
1295
1410
  }
1296
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
+
1297
1436
  module.exports = {
1437
+ dnsNameProblem: dnsNameProblem,
1298
1438
  pemDecode: pemDecode,
1299
1439
  pemDecodeAll: pemDecodeAll,
1300
1440
  pemEncode: pemEncode,
@@ -1306,6 +1446,8 @@ module.exports = {
1306
1446
  DN_SHORT: DN_SHORT,
1307
1447
  time: time,
1308
1448
  algorithmIdentifier: algorithmIdentifier,
1449
+ pbkdf2Params: pbkdf2Params,
1450
+ pbmac1Params: pbmac1Params,
1309
1451
  spki: spki,
1310
1452
  makeParser: makeParser,
1311
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.24",
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:342eb0a5-bbfa-46f5-9b51-912a21d97f53",
5
+ "serialNumber": "urn:uuid:2d9fa8bb-e93e-450a-991c-ef2f3b3fb9e2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-26T21:06:05.598Z",
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.24",
22
+ "bom-ref": "@blamejs/pki@0.3.26",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.24",
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.24",
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.24",
57
+ "ref": "@blamejs/pki@0.3.26",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]