@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/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
@@ -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
- if (Buffer.isBuffer(k)) return k;
199
- if (k instanceof Uint8Array) return Buffer.from(k);
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 = Buffer.from(key); owned = true; }
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
- return guard.async.deferred(function () { return _sign(messageImprint, tsa, opts); });
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) {
@@ -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");
@@ -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 Promise.resolve().then(function () { return _verifyMetadataBlob(blob, opts); });
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 Buffer.from(v);
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
- return Promise.resolve().then(function () { return _sign(spec, issuer, opts); });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
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:f56149c0-58f9-40e1-a0f0-36a0dd2e4ee9",
5
+ "serialNumber": "urn:uuid:a8c014a2-3631-443a-b33d-f10501d992fb",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-16T03:23:32.715Z",
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.6",
22
+ "bom-ref": "@blamejs/pki@0.5.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.5.6",
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.6",
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.6",
57
+ "ref": "@blamejs/pki@0.5.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]