@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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/index.js +5 -4
- package/lib/acme.js +8 -5
- package/lib/cmp-build.js +14 -1
- package/lib/cmp-verify.js +705 -0
- package/lib/est.js +29 -12
- package/lib/lint.js +3 -26
- package/lib/path-validate.js +8 -0
- package/lib/schema-pkcs12.js +5 -66
- package/lib/schema-pkix.js +117 -0
- package/lib/webcrypto.js +31 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
-
|
|
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:
|
|
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
|
|
728
|
-
//
|
|
729
|
-
//
|
|
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
|
-
|
|
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
|
|
195
|
-
//
|
|
196
|
-
|
|
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
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
package/lib/schema-pkcs12.js
CHANGED
|
@@ -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
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
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 }.
|
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
|
|
@@ -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
|
-
|
|
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
|
]
|