@blamejs/pki 0.5.6 → 0.5.7
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 +23 -1
- package/MIGRATING.md +22 -0
- package/lib/attrcert-sign.js +5 -1
- package/lib/cmc-build.js +8 -13
- package/lib/cmc-verify.js +2 -2
- package/lib/cmp-build.js +7 -2
- package/lib/cmp-session.js +4 -2
- package/lib/cmp-verify.js +1 -1
- package/lib/cms-compress.js +1 -2
- package/lib/cms-decrypt.js +2 -4
- package/lib/cms-encrypt.js +1 -2
- package/lib/cms-sign.js +33 -6
- package/lib/cms-verify.js +46 -9
- package/lib/crl-sign.js +9 -3
- package/lib/crmf-sign.js +5 -1
- package/lib/csr-sign.js +5 -1
- package/lib/est.js +5 -4
- package/lib/guard-bytes.js +368 -5
- package/lib/guard-parsed.js +71 -2
- package/lib/ocsp.js +19 -7
- package/lib/pkcs12-build.js +21 -6
- package/lib/pki-build.js +2 -3
- package/lib/schema-cms.js +133 -0
- package/lib/sign-scheme.js +7 -4
- package/lib/tsp-sign.js +5 -1
- package/lib/validator-tpm.js +1 -1
- package/lib/webauthn-mds.js +1 -1
- package/lib/webauthn.js +1 -1
- package/lib/x509-sign.js +11 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-cms.js
CHANGED
|
@@ -212,6 +212,138 @@ function _checkContentBindingAttrs(attrs, mode) {
|
|
|
212
212
|
if (md === 0) throw NS.E("cms/missing-message-digest", "the attribute set must contain a message-digest attribute (RFC 5652 sec. 11.2)");
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// looksLikeSignedAttributes(bytes) -> boolean.
|
|
216
|
+
//
|
|
217
|
+
// Does `bytes` parse as a DER SignedAttributes block -- a SET OF Attribute carrying BOTH the
|
|
218
|
+
// content-type and message-digest attributes RFC 5652 sec. 5.3 makes mandatory whenever signed
|
|
219
|
+
// attributes are present?
|
|
220
|
+
//
|
|
221
|
+
// This is the detector for the signed-attribute stripping forgery
|
|
222
|
+
// (draft-vangeest-lamps-cms-euf-cma-signeddata, Attack Type 1). A CMS signature does not commit to
|
|
223
|
+
// WHETHER signed attributes were present, so a signature made over a SignedAttributes block can be
|
|
224
|
+
// re-presented as one made over content: drop the signedAttrs field and set the encapsulated
|
|
225
|
+
// content to the DER of those same attributes. Sec. 5.4 then says the signature is over the content
|
|
226
|
+
// itself, which is exactly what it covers, and with no attributes there is no message-digest or
|
|
227
|
+
// content-type attribute left to disagree. The proposed standards fixes are protocol changes -- a
|
|
228
|
+
// context string naming which mode was signed -- that no verifier can apply on its own.
|
|
229
|
+
//
|
|
230
|
+
// What a verifier CAN do is refuse the shape. Every message produced by the attack has, as its
|
|
231
|
+
// content, the encoded SignedAttributes of a real message, and sec. 5.3 requires those to carry
|
|
232
|
+
// both attributes named above. That makes their presence a NECESSARY condition of the attack rather
|
|
233
|
+
// than a guess, and the shape is one ordinary content does not have: a certificate, a JSON payload,
|
|
234
|
+
// arbitrary bytes, and even a SET OF other attributes all fail it. The cost is a message whose
|
|
235
|
+
// legitimate content really is an encoded SignedAttributes block signed WITHOUT attributes, which
|
|
236
|
+
// is refused as genuinely ambiguous -- sign it with attributes and it is unambiguous again.
|
|
237
|
+
//
|
|
238
|
+
// One mandatory attribute value, held to all three conditions a real SignedAttributes meets:
|
|
239
|
+
// exactly one value (RFC 5652 sec. 11.1 / sec. 11.2 make both single-valued), the right tag, and a
|
|
240
|
+
// body that actually READS as that type. All three together, for each attribute -- checking the
|
|
241
|
+
// cardinality and the tag while letting an undecodable body through would refuse content the real
|
|
242
|
+
// SignedAttributes parser could never have produced, which is the false positive this whole
|
|
243
|
+
// detector is shaped to avoid.
|
|
244
|
+
function _readsAs(vals, tagNumber, reader) {
|
|
245
|
+
if (vals.length !== 1 || !schema.isUniversal(vals[0], tagNumber)) return false;
|
|
246
|
+
var reads = true;
|
|
247
|
+
try { reader(vals[0]); }
|
|
248
|
+
catch (_e) {
|
|
249
|
+
reads = false; // right tag, unreadable body -- not a preimage anything signed
|
|
250
|
+
}
|
|
251
|
+
return reads;
|
|
252
|
+
}
|
|
253
|
+
// SigningTime ::= Time is a CHOICE of UTCTime and GeneralizedTime, so the tag is one of two and the
|
|
254
|
+
// reader settles which -- _readsAs pins a single tag and cannot express it.
|
|
255
|
+
function _readsAsTime(vals) {
|
|
256
|
+
if (vals.length !== 1) return false;
|
|
257
|
+
var reads = true;
|
|
258
|
+
try { asn1.read.time(vals[0]); }
|
|
259
|
+
catch (_e) {
|
|
260
|
+
reads = false;
|
|
261
|
+
}
|
|
262
|
+
return reads;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Deliberately a total function: it answers about arbitrary attacker bytes and must never throw.
|
|
266
|
+
// The X.690 sec. 11.6 SET OF ordering rule, as the schema engine applies it: each component's
|
|
267
|
+
// encoding is greater than or equal to the one before it.
|
|
268
|
+
function _ascendingDer(nodes) {
|
|
269
|
+
for (var i = 1; i < nodes.length; i++) {
|
|
270
|
+
if (Buffer.compare(nodes[i - 1].bytes, nodes[i].bytes) > 0) return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function looksLikeSignedAttributes(bytes) {
|
|
276
|
+
if (!bytes || !bytes.length) return false;
|
|
277
|
+
var node;
|
|
278
|
+
try { node = asn1.decode(bytes); }
|
|
279
|
+
catch (_e) { return false; } // not DER at all -- not this shape
|
|
280
|
+
if (!schema.isUniversal(node, asn1.TAGS.SET) || !node.constructed) return false;
|
|
281
|
+
var kids = node.children || [];
|
|
282
|
+
if (!kids.length) return false;
|
|
283
|
+
// Every rule _checkContentBindingAttrs applies to a REAL SignedAttributes is applied here, and
|
|
284
|
+
// for one reason: the detector must match only what a conforming block can be. A set the real
|
|
285
|
+
// parser would have rejected cannot be the preimage of any signature, so matching it would refuse
|
|
286
|
+
// content no attack could have produced. Enumerated against that function rather than discovered
|
|
287
|
+
// one rule at a time -- no duplicate attribute types (sec. 5.3), content-type single-valued and a
|
|
288
|
+
// readable OID (sec. 11.1), message-digest single-valued and a readable OCTET STRING (sec. 11.2),
|
|
289
|
+
// signing-time when present single-valued and a readable Time (sec. 11.3).
|
|
290
|
+
// X.690 sec. 11.6: the components of a SET OF appear in ascending DER order, and the schema
|
|
291
|
+
// engine enforces exactly that on the real SignedAttributes. An out-of-order set is one the
|
|
292
|
+
// walker refuses, so it is not the preimage of any signature either.
|
|
293
|
+
if (!_ascendingDer(kids)) return false;
|
|
294
|
+
var sawContentType = false, sawMessageDigest = false, seenTypes = Object.create(null);
|
|
295
|
+
for (var i = 0; i < kids.length; i++) {
|
|
296
|
+
var a = kids[i];
|
|
297
|
+
// Attribute ::= SEQUENCE { attrType OBJECT IDENTIFIER, attrValues SET OF ANY }
|
|
298
|
+
if (!schema.isUniversal(a, asn1.TAGS.SEQUENCE)) return false;
|
|
299
|
+
if (!a.children || a.children.length !== 2) return false;
|
|
300
|
+
var t = a.children[0], vs = a.children[1];
|
|
301
|
+
if (!schema.isUniversal(t, asn1.TAGS.OBJECT_IDENTIFIER)) return false;
|
|
302
|
+
if (!schema.isUniversal(vs, asn1.TAGS.SET)) return false;
|
|
303
|
+
var attrOid;
|
|
304
|
+
try { attrOid = asn1.read.oid(t); }
|
|
305
|
+
catch (_e2) { return false; }
|
|
306
|
+
if (seenTypes[attrOid]) return false; // a repeated attribute type -- sec. 5.3 forbids it
|
|
307
|
+
seenTypes[attrOid] = true;
|
|
308
|
+
// The sec. 11 PLACEMENT rows, from the same table the real parser reads. An attribute the
|
|
309
|
+
// parser refuses to see in signedAttrs -- id-countersignature is the one sec. 11.4 names --
|
|
310
|
+
// cannot appear in a conforming SignedAttributes, so a set containing it is not a preimage any
|
|
311
|
+
// signature covers. Missing this row was the difference between a necessary condition and a
|
|
312
|
+
// guess: it would have refused ordinary content that merely carried that attribute encoding.
|
|
313
|
+
var placement = ATTR_FORBIDDEN_IN[attrOid];
|
|
314
|
+
if (placement && placement.signed) return false;
|
|
315
|
+
// RFC 5652 gives every AttributeValue set SIZE (1..MAX), so an attribute with an EMPTY value
|
|
316
|
+
// set is one no conforming signer produced and no signature covers. The upper bound is
|
|
317
|
+
// deliberately not applied: this decoder caps values per attribute as a resource limit of its
|
|
318
|
+
// own, and a limit this implementation chose is not a fact about what a signature can cover.
|
|
319
|
+
// An external signer may sign a conforming set larger than that cap, and the stripped message
|
|
320
|
+
// presents those bytes as opaque content where the cap never applies -- refusing to recognize
|
|
321
|
+
// it because of a local limit would miss exactly the preimage the attack reuses.
|
|
322
|
+
var n = (vs.children || []).length;
|
|
323
|
+
if (n < 1) return false;
|
|
324
|
+
if (!_ascendingDer(vs.children || [])) return false; // the inner SET OF is ordered too
|
|
325
|
+
// The two mandatory attributes are checked down to their VALUES, not just their type OIDs.
|
|
326
|
+
// RFC 5652 sec. 11.1 makes content-type a single OBJECT IDENTIFIER and sec. 11.2 makes
|
|
327
|
+
// message-digest a single OCTET STRING, so a set carrying those OIDs over an empty or
|
|
328
|
+
// wrongly-typed value CANNOT be the preimage of a real signature -- and refusing it would be a
|
|
329
|
+
// false positive on content that merely resembles the shape. The detector has to stay a
|
|
330
|
+
// necessary condition of the attack; anything broader costs a legitimate caller.
|
|
331
|
+
var vals = vs.children || [];
|
|
332
|
+
if (attrOid === OID_CONTENT_TYPE) {
|
|
333
|
+
if (!_readsAs(vals, asn1.TAGS.OBJECT_IDENTIFIER, asn1.read.oid)) return false;
|
|
334
|
+
sawContentType = true;
|
|
335
|
+
} else if (attrOid === OID_MESSAGE_DIGEST) {
|
|
336
|
+
if (!_readsAs(vals, asn1.TAGS.OCTET_STRING, asn1.read.octetString)) return false;
|
|
337
|
+
sawMessageDigest = true;
|
|
338
|
+
} else if (attrOid === OID_SIGNING_TIME) {
|
|
339
|
+
// Not mandatory, but when present sec. 11.3 constrains it the same way, so a set carrying an
|
|
340
|
+
// unreadable signing-time is one the real parser would have refused.
|
|
341
|
+
if (!_readsAsTime(vals)) return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return sawContentType && sawMessageDigest;
|
|
345
|
+
}
|
|
346
|
+
|
|
215
347
|
// RFC 5652 sec. 5.3 / sec. 9.3 -- when a content-type attribute is present, it MUST
|
|
216
348
|
// be single-valued (sec. 11.1) and its value MUST equal the eContentType (a
|
|
217
349
|
// cross-field consistency both parsed here). Shared by SignedData signedAttrs,
|
|
@@ -1310,6 +1442,7 @@ module.exports = {
|
|
|
1310
1442
|
walkSignedData: walkSignedData,
|
|
1311
1443
|
walkEncryptedData: walkEncryptedData,
|
|
1312
1444
|
walkCountersignature: walkCountersignature,
|
|
1445
|
+
looksLikeSignedAttributes: looksLikeSignedAttributes,
|
|
1313
1446
|
assertAttachedCiphertext: assertAttachedCiphertext,
|
|
1314
1447
|
// The structure's own algorithm tables, exported (like the walk* helpers) as the single source
|
|
1315
1448
|
// of truth the crypto layer (cms-encrypt / cms-decrypt) shares -- so the wrap<->KEK-length, the
|
package/lib/sign-scheme.js
CHANGED
|
@@ -195,8 +195,11 @@ function _assertKeyMatchesScheme(key, imp, E) {
|
|
|
195
195
|
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
196
196
|
}
|
|
197
197
|
function _normPkcs8(k, label, E) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// A caller's own Buffer is BORROWED, not copied -- the same rule _importKey states below. Every
|
|
199
|
+
// other form is copied, and neither copy is duplicated further, so a private key never gains a
|
|
200
|
+
// plaintext duplicate this module cannot account for.
|
|
201
|
+
if (Buffer.isBuffer(k)) return guard.bytes.view(k, E, "bad-input", label);
|
|
202
|
+
if (k instanceof Uint8Array) return guard.bytes.snapshot(k, E, "bad-input", label);
|
|
200
203
|
if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw E("bad-input", label + " PEM could not be decoded", e); } }
|
|
201
204
|
throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
202
205
|
}
|
|
@@ -219,8 +222,8 @@ function _importKey(key, imp, E) {
|
|
|
219
222
|
// only thing that reads it. A caller's own Buffer is passed through untouched: they hold a live
|
|
220
223
|
// reference and will use it again, so clearing it would destroy their key.
|
|
221
224
|
var der, owned = false;
|
|
222
|
-
if (Buffer.isBuffer(key)) der = key;
|
|
223
|
-
else if (key instanceof Uint8Array) { der =
|
|
225
|
+
if (Buffer.isBuffer(key)) der = guard.bytes.view(key, E, "bad-input", "the signer private key");
|
|
226
|
+
else if (key instanceof Uint8Array) { der = guard.bytes.snapshot(key, E, "bad-input", "the signer private key"); owned = true; }
|
|
224
227
|
else if (typeof key === "string") {
|
|
225
228
|
try { der = pkcs8.pemDecode(key); }
|
|
226
229
|
catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
|
package/lib/tsp-sign.js
CHANGED
|
@@ -134,7 +134,11 @@ function _signingCertV2(certDer, hashName) {
|
|
|
134
134
|
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async); the checks stay
|
|
135
135
|
// synchronous because they read the caller's mutable imprint, TSA material and options.
|
|
136
136
|
function sign(messageImprint, tsa, opts) {
|
|
137
|
-
|
|
137
|
+
// Every caller-owned argument copied at entry and released when the call settles -- see the note
|
|
138
|
+
// on the same call in x509-sign. The imprint matters most: it is the digest the token attests to.
|
|
139
|
+
return guard.bytes.fixedCall(TspError, "tsp/bad-input", [
|
|
140
|
+
[messageImprint, "the messageImprint"], [tsa, "the TSA"], [opts, "pki.tsp.sign options"],
|
|
141
|
+
], _sign);
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function _sign(messageImprint, tsa, opts) {
|
package/lib/validator-tpm.js
CHANGED
|
@@ -225,7 +225,7 @@ function normalizeObjectAttributePolicy(policy, E, code) {
|
|
|
225
225
|
// canonical round-trip -- the same three checks written here by hand, and now written
|
|
226
226
|
// once. It also decodes, so the hex path cannot validate one string and decode another.
|
|
227
227
|
allow = ap.allow.map(function (entry, i) {
|
|
228
|
-
if (Buffer.isBuffer(entry)) return entry;
|
|
228
|
+
if (Buffer.isBuffer(entry)) return guard.bytes.snapshot(entry, E, code, "opts.tpmPolicy.authPolicy.allow[" + i + "]");
|
|
229
229
|
var label = "opts.tpmPolicy.authPolicy.allow[" + i + "]";
|
|
230
230
|
if (typeof entry !== "string" || entry.length === 0) {
|
|
231
231
|
throw new E(code, label + " must be a Buffer or an even-length hex string");
|
package/lib/webauthn-mds.js
CHANGED
|
@@ -233,7 +233,7 @@ function _asCert(v, label) {
|
|
|
233
233
|
// with this toolkit: which metadata authority to trust is the operator's choice, and a verifier
|
|
234
234
|
// that bundled its own would be deciding trust on the caller's behalf.
|
|
235
235
|
function verifyMetadataBlob(blob, opts) {
|
|
236
|
-
return
|
|
236
|
+
return guard.async.deferred(function () { return _verifyMetadataBlob(blob, opts); });
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function _verifyMetadataBlob(blob, opts) {
|
package/lib/webauthn.js
CHANGED
|
@@ -531,7 +531,7 @@ var _FORMAT_SCOPED_BOOLEAN_OPTS = ["verifySafetyNetJws", "requireCtsProfileMatch
|
|
|
531
531
|
// object cannot recurse without end.
|
|
532
532
|
function _cloneParsed(v, depth) {
|
|
533
533
|
if (depth > 64) throw _err("webauthn/bad-input", "opts.rootCertificates[] is nested too deeply to be a parsed certificate");
|
|
534
|
-
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return
|
|
534
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, _err, "webauthn/bad-input", "a byte field of opts.rootCertificates[]");
|
|
535
535
|
if (Array.isArray(v)) return v.map(function (x) { return _cloneParsed(x, depth + 1); });
|
|
536
536
|
if (v instanceof Date) return new Date(v.getTime());
|
|
537
537
|
if (v && typeof v === "object") {
|
package/lib/x509-sign.js
CHANGED
|
@@ -78,7 +78,7 @@ function _skiValueOf(caCert) {
|
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
function _akiKeyId(val, ctx) {
|
|
81
|
-
if (Buffer.isBuffer(val)) return val;
|
|
81
|
+
if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, CertificateError, "x509/bad-input", "the authorityKeyIdentifier keyIdentifier");
|
|
82
82
|
if (val === true) {
|
|
83
83
|
if (ctx.issuerCert) { var ski = _skiValueOf(ctx.issuerCert); if (ski) return ski; }
|
|
84
84
|
return _spkiKeyId(ctx.issuerSpki);
|
|
@@ -265,7 +265,16 @@ function _hasCriticalSan(extSpec) {
|
|
|
265
265
|
* pki.schema.x509.parse(root).subject.dn; // "CN=Example Root CA"
|
|
266
266
|
*/
|
|
267
267
|
function sign(spec, issuer, opts) {
|
|
268
|
-
|
|
268
|
+
// EVERY caller-owned argument is copied before a field of any of them is read, and every copy is
|
|
269
|
+
// cleared when the call settles. The checks below run NOW, and the values they approved are
|
|
270
|
+
// encoded, signed and emitted several promise turns afterwards, while the caller still owns all
|
|
271
|
+
// three objects -- so a spec, an option or a signer read again after the first turn need not be
|
|
272
|
+
// the one that passed. Copying only some of them leaves the rest reachable; copying without the
|
|
273
|
+
// release leaves a duplicate of whatever secret was nested in them. guard.bytes.fixArguments is
|
|
274
|
+
// both halves, and is what every producing verb in the toolkit opens with.
|
|
275
|
+
return guard.bytes.fixedCall(CertificateError, "x509/bad-input", [
|
|
276
|
+
[spec, "the certificate spec"], [issuer, "the issuer"], [opts, "pki.x509.sign options"],
|
|
277
|
+
], _sign);
|
|
269
278
|
}
|
|
270
279
|
|
|
271
280
|
function _sign(spec, issuer, opts) {
|
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:a8c014a2-3631-443a-b33d-f10501d992fb",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-16T09:48:05.398Z",
|
|
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.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.7",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.7",
|
|
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.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.7",
|
|
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.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.7",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|