@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.
- package/CHANGELOG.md +25 -0
- package/README.md +2 -2
- package/index.js +8 -5
- package/lib/acme.js +8 -5
- package/lib/cmp-build.js +14 -1
- package/lib/cmp-verify.js +705 -0
- package/lib/constants.js +15 -0
- package/lib/est.js +32 -45
- package/lib/http-transport.js +95 -6
- package/lib/inspect.js +20 -0
- package/lib/lint.js +3 -26
- package/lib/path-validate.js +294 -19
- package/lib/schema-cms.js +45 -0
- package/lib/schema-pkcs12.js +5 -66
- package/lib/schema-pkix.js +142 -0
- package/lib/webcrypto.js +31 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-pkix.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:2d9fa8bb-e93e-450a-991c-ef2f3b3fb9e2",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.26",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|