@blamejs/pki 0.5.5 → 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/pbes2.js CHANGED
@@ -177,7 +177,12 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
177
177
  // <prefix>/bad-input a param guard raises is normalized to the structural <prefix>/bad-algorithm-parameters.
178
178
  // A wrong key / bad PKCS#7 pad collapses to the UNIFORM <prefix>/decrypt-failed (RFC 8018 sec. 8). The
179
179
  // plaintext integrity re-check (re-parse as a PrivateKeyInfo / SafeContents) is the CALLER's step.
180
- function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
180
+ // `budget` (optional) is a shared { rounds } tally a caller decrypting MANY structures under one
181
+ // call charges against, so the per-structure iteration cap cannot simply reset each time: PBKDF2
182
+ // runs on the event loop, and a store that repeats a costly bag up to the parser's element limit
183
+ // otherwise multiplies the cap by that limit. A caller decrypting exactly one structure passes
184
+ // nothing and is bounded by the cap alone.
185
+ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix, budget) {
181
186
  var keyBits, iv, pb;
182
187
  try {
183
188
  var p = seqChildren(params, 2, "PBES2 parameters", E, prefix);
@@ -197,6 +202,11 @@ function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
197
202
  }
198
203
  throw E(prefix + "/bad-algorithm-parameters", "malformed PBES2 parameters", e);
199
204
  }
205
+ // Charge the shared budget BEFORE deriving, so the work is refused rather than performed.
206
+ if (budget) {
207
+ budget.rounds -= pb.iterations;
208
+ if (budget.rounds < 0) throw E(prefix + "/iteration-limit", "the aggregate PBKDF2 key-derivation work exceeds the budget (a hostile many-element input)");
209
+ }
200
210
  var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
201
211
  try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
202
212
  catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
@@ -69,12 +69,14 @@ var MAX_PBMAC1_KEYLEN = 1024; // an HMAC key beyond a hash block is pointless
69
69
  // pkcs12-local (its KDF is bespoke here) while PBMAC1 reuses the toolkit-wide PBKDF2 ceiling. 1e6 is ~500x
70
70
  // the OpenSSL default (2048) yet still bounds the loop to ~1 second.
71
71
  var CLASSIC_MAC_MAX_ITERATIONS = 1000000;
72
- // The AGGREGATE cap on synchronous legacy-PBE App. B KDF work across a single open() -- SHA-1 rounds summed
73
- // over every legacy bag (its KDF block count x iterations). The per-bag iteration cap resets per bag, so a
74
- // hostile store that duplicates a costly legacy bag up to the parser's 1024-bag limit would otherwise block
75
- // the event loop for minutes; this bounds the total to ~the classic MAC ceiling. A conforming store runs only
76
- // a few thousand rounds (its handful of bags at ~2048 iterations).
77
- var LEGACY_KDF_MAX_ROUNDS = CLASSIC_MAC_MAX_ITERATIONS;
72
+ // The AGGREGATE cap on synchronous KDF work across a single open() -- rounds summed over every encrypted
73
+ // bag and safe, whichever scheme it uses: the legacy App. B KDF (its block count x iterations) and PBKDF2
74
+ // alike. The per-bag iteration cap resets per bag, so a hostile store that duplicates a costly bag up to the
75
+ // parser's 1024-element limit would otherwise block the event loop for minutes; this bounds the total to
76
+ // ~the classic MAC ceiling. It covers BOTH schemes because covering only the legacy one left the modern
77
+ // path -- the one every current producer emits -- free to multiply its cap by the element limit.
78
+ // A conforming store runs only a few thousand rounds (its handful of bags at ~2048 iterations).
79
+ var KDF_MAX_ROUNDS = CLASSIC_MAC_MAX_ITERATIONS;
78
80
 
79
81
  // The classic App. B.2 KDF (u = hash output bytes, v = compression block bytes) per RFC 7292 App. B.2.
80
82
  var P12_KDF_UV = {
@@ -102,19 +104,32 @@ var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "s
102
104
  // is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
103
105
  function _p12Password(pw) { return _p12PasswordOwned(pw).bytes; }
104
106
 
107
+ // An ABSENT password is refused rather than encoded as the empty one. The two are not the same
108
+ // credential, and the difference is invisible at the call site: a caller who misspells the option,
109
+ // or threads it through a layer that drops it, otherwise gets a store whose private key is
110
+ // protected by nothing and no error anywhere saying so. The empty password remains available --
111
+ // it just has to be asked for, as "".
112
+ var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- an omitted password " +
113
+ "is not the empty password; pass \"\" to use the empty one deliberately";
114
+
115
+ // Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
116
+ // capability cannot arrive with its option silently ignored at this boundary.
117
+ var _INTEGRITY_OPTS = { mode: 1, signer: 1, signers: 1, certificates: 1, sid: 1, signingTime: 1 };
118
+
105
119
  // The same encoding, reporting OWNERSHIP -- mirroring pbes2.passwordBytesOwned. A caller-supplied
106
120
  // Buffer is returned AS-IS and is BORROWED: clearing it would destroy the caller's own credential,
107
121
  // which is a worse defect than leaving a copy readable. Every other input is re-encoded into a
108
122
  // buffer this module allocated, which it must clear once a derivation has consumed it.
109
123
  function _p12PasswordOwned(pw) {
110
- if (Buffer.isBuffer(pw)) return { bytes: pw, owned: false };
124
+ if (Buffer.isBuffer(pw)) return { bytes: guard.bytes.view(pw, Pkcs12Error, "pkcs12/bad-input", "the password"), owned: false };
111
125
  return { bytes: _p12Encode(pw), owned: true };
112
126
  }
113
127
  function _p12Encode(pw) {
114
- if (pw == null) pw = "";
115
- if (Buffer.isBuffer(pw)) return pw;
116
- if (pw instanceof Uint8Array) return Buffer.from(pw);
117
- if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
128
+ // A view first, so a detached backing store is a reject rather than an empty password; then a COPY,
129
+ // because everything this function returns is reported OWNED and gets zeroized after derivation --
130
+ // handing back a view of a caller's Uint8Array would wipe the caller's own credential.
131
+ if (Buffer.isBuffer(pw) || pw instanceof Uint8Array) return guard.bytes.snapshot(pw, Pkcs12Error, "pkcs12/bad-input", "the password");
132
+ if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
118
133
  var out = Buffer.alloc(pw.length * 2 + 2); // + the 2-byte NULL terminator
119
134
  for (var i = 0; i < pw.length; i++) {
120
135
  var u = pw.charCodeAt(i);
@@ -130,12 +145,19 @@ function _p12Encode(pw) {
130
145
  // PBKDF2 the raw UTF-8 password for these modern schemes (confirmed byte-for-byte against `openssl pkcs12`),
131
146
  // reserving the BMPString+NULL form for the bespoke Appendix B KDF only. A file we emit must open in OpenSSL,
132
147
  // so the modern schemes use UTF-8 here; only the classic Appendix B MAC uses `_p12Password`.
133
- function _pbePassword(pw) {
134
- if (pw == null) pw = "";
135
- if (Buffer.isBuffer(pw)) return pw;
136
- if (pw instanceof Uint8Array) return Buffer.from(pw);
137
- if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
138
- return Buffer.from(pw, "utf8");
148
+ // The UTF-8 encoding, reporting OWNERSHIP -- the sibling of _p12PasswordOwned, and it exists
149
+ // for the same reason. A caller-supplied Buffer is BORROWED and left alone; every other input is
150
+ // re-encoded into a buffer this module allocated, and a plaintext password copy this module made
151
+ // is cleared once the derivation has consumed it. Without it the App. B.1 copy taken from the same
152
+ // argument in the same call was wiped while this one was not.
153
+ function _pbePasswordOwned(pw) {
154
+ if (Buffer.isBuffer(pw)) return { bytes: guard.bytes.view(pw, Pkcs12Error, "pkcs12/bad-input", "the password"), owned: false };
155
+ if (pw instanceof Uint8Array) return { bytes: guard.bytes.snapshot(pw, Pkcs12Error, "pkcs12/bad-input", "the password"), owned: true };
156
+ if (typeof pw !== "string") throw _err("pkcs12/bad-input", _MISSING_PASSWORD);
157
+ return { bytes: Buffer.from(pw, "utf8"), owned: true };
158
+ }
159
+ function _wipePw(owned) {
160
+ if (owned.owned) guard.secret.zeroize(owned.bytes, Pkcs12Error, "pkcs12/bad-input", "the password encoding");
139
161
  }
140
162
 
141
163
  // Concatenate copies of `src` to the smallest positive multiple of `blockSize` (>= src length), truncating
@@ -267,8 +289,10 @@ function _buildBag(bag, opts, depth) {
267
289
  var kDer = _coerceDer(bag.key, "shroudedKey key");
268
290
  try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
269
291
  var enc = bag.encrypt || {};
270
- var pw = _pbePassword(enc.password != null ? enc.password : opts.password);
271
- var r = pbes2.pbes2Encrypt(pw, kDer, _pbeOpts(enc), _err, "pkcs12");
292
+ var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
293
+ var r;
294
+ try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, _pbeOpts(enc), _err, "pkcs12"); }
295
+ finally { _wipePw(pw); }
272
296
  return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
273
297
  }
274
298
  case "cert": {
@@ -376,8 +400,10 @@ async function _buildAuthSafeElement(sc, opts) {
376
400
  return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
377
401
  }
378
402
  if (!sc.encrypt || typeof sc.encrypt !== "object") throw _err("pkcs12/bad-input", "safeContents.encrypt must be an object { password? } (RFC 7292 sec. 5.1) -- omit it entirely for a plaintext safe");
379
- var pw = _pbePassword(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
380
- var r = pbes2.pbes2Encrypt(pw, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12");
403
+ var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
404
+ var r;
405
+ try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12"); }
406
+ finally { _wipePw(pw); }
381
407
  var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
382
408
  var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
383
409
  return b.sequence([b.oid(O("encryptedData")), b.explicit(0, encData)]);
@@ -420,7 +446,10 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
420
446
  var iter2 = _assertMacIter(macOpts.iterations == null ? DEFAULT_PBMAC1_ITER : macOpts.iterations, C.LIMITS.PBKDF2_MAX_ITERATIONS);
421
447
  var keyLen = macOpts.keyLength != null ? macOpts.keyLength : prf.keyLen;
422
448
  if (typeof keyLen !== "number" || !Number.isInteger(keyLen) || keyLen < 20 || keyLen > MAX_PBMAC1_KEYLEN) throw _err("pkcs12/bad-input", "PBMAC1 keyLength must be an integer in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
423
- var mac = await pbes2.pbmac1(_pbePassword(password), salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
449
+ var macPw2 = _pbePasswordOwned(password);
450
+ var mac;
451
+ try { mac = await pbes2.pbmac1(macPw2.bytes, salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); } // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
452
+ finally { _wipePw(macPw2); }
424
453
  var desc = { salt: salt, iterationCount: iter2, keyLength: keyLen, prfName: prf.prfName, macName: prf.prfName };
425
454
  var digestInfo2 = b.sequence([pbes2.pbmac1AlgId(desc), b.octetString(mac)]);
426
455
  // MacData.macSalt + iterations are ignored on a PBMAC1 verify but MUST be present + non-1 (RFC 9579 4c/4d).
@@ -496,9 +525,33 @@ function _normalizeSpec(spec, opts) {
496
525
  * { type: 'shroudedKey', key: signerKeyPkcs8, encrypt: { password: 'changeit' } } ] }] },
497
526
  * { password: 'changeit', mac: { algorithm: 'hmac', hash: 'sha256' } });
498
527
  */
499
- async function build(spec, opts) {
528
+ function build(spec, opts) {
529
+ // Both arguments copied at entry and released when the call settles -- see the note on the same
530
+ // call in x509-sign. A PKCS#12 file is assembled over many promise turns (a key derivation per
531
+ // bag, then the outer MAC) while the caller still owns the object holding the passwords and the
532
+ // bag contents. Rewriting a password buffer partway through produced a file whose MAC and whose
533
+ // bag encryption were keyed to two different values, so it opened with neither the password
534
+ // passed in nor the one written over it. The release is what keeps the copy of that password
535
+ // from outliving the call.
536
+ return guard.bytes.fixedCall(Pkcs12Error, "pkcs12/bad-input", [
537
+ [spec, "the PKCS#12 spec"], [opts, "pki.pkcs12.build options"],
538
+ ], _build);
539
+ }
540
+
541
+ async function _build(spec, opts) {
500
542
  opts = opts || {};
501
- var pubKey = opts.integrity != null && opts.integrity.mode === "public-key";
543
+ // The integrity mode is checked against the permitted set rather than compared to one literal.
544
+ // Compared, any other spelling reads as "not public-key" and silently selects password integrity,
545
+ // dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
546
+ // signature, and nothing in the store or the call says which they got.
547
+ if (opts.integrity != null) {
548
+ if (typeof opts.integrity !== "object" || Array.isArray(opts.integrity)) throw _err("pkcs12/bad-input", "opts.integrity must be an object { mode, signer|signers, ... }");
549
+ guard.identifier.assertKnownKeys(opts.integrity, _INTEGRITY_OPTS, _err, "pkcs12/bad-input", "opts.integrity has an unknown option ");
550
+ if (opts.integrity.mode !== "public-key") {
551
+ throw _err("pkcs12/bad-integrity-mode", "opts.integrity.mode must be \"public-key\" (the only mode it selects); omit opts.integrity for password integrity, got " + JSON.stringify(opts.integrity.mode));
552
+ }
553
+ }
554
+ var pubKey = opts.integrity != null;
502
555
  // RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
503
556
  // config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
504
557
  if (pubKey && opts.mac != null && opts.mac !== false) throw _err("pkcs12/bad-integrity-mode", "public-key integrity has no MacData -- do not combine opts.mac with opts.integrity.mode 'public-key' (RFC 7292 sec. 4)");
@@ -590,7 +643,9 @@ async function _verifyMacOfStore(m, password, opts) {
590
643
  // downgraded store cannot pass under a weak MAC even though the algorithm identifiers parse.
591
644
  if (prfWc === "SHA-1" || macWc === "SHA-1") throw _err("pkcs12/unsupported-algorithm", "PBMAC1 with a <= 160-bit digest (SHA-1) is refused (RFC 9579 sec. 5/7)");
592
645
  _capWork(kdf.iterationCount, kdf.salt, opts, kdf.keyLength, C.LIMITS.PBKDF2_MAX_ITERATIONS);
593
- computed = await pbes2.pbmac1(_pbePassword(password), kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); // PBKDF2 -> UTF-8
646
+ var vPw = _pbePasswordOwned(password);
647
+ try { computed = await pbes2.pbmac1(vPw.bytes, kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); } // PBKDF2 -> UTF-8
648
+ finally { _wipePw(vPw); }
594
649
  }
595
650
  return computed.length === expected.length && guard.crypto.constantTimeEqual(computed, expected);
596
651
  }
@@ -687,7 +742,7 @@ async function open(pfx, password, opts) {
687
742
  // wrong bag password fails at the first encrypted bag as the uniform pkcs12/decrypt-failed.
688
743
  var out = { integrityMode: m.integrityMode, macVerified: macVerified, signers: signers, keys: [], certs: [], crls: [], secrets: [] };
689
744
  var i;
690
- var kdfBudget = { rounds: LEGACY_KDF_MAX_ROUNDS }; // aggregate legacy-PBE KDF work budget for this whole open()
745
+ var kdfBudget = { rounds: KDF_MAX_ROUNDS }; // aggregate KDF work budget for this whole open(), both schemes
691
746
  for (i = 0; i < m.safeBags.length; i++) _openBag(m.safeBags[i], password, opts, out, 0, kdfBudget);
692
747
  for (i = 0; i < m.encryptedSafes.length; i++) await _openEncryptedSafe(m.encryptedSafes[i], password, opts, out, 0, kdfBudget);
693
748
  if (opts.keys === "crypto") {
@@ -801,7 +856,15 @@ function _decryptLegacyPbe(ea, ct, password, opts, budget) {
801
856
  // Decrypt a PBES2 (RFC 8018) or legacy-PBE (RFC 7292 App. C) bag/safe -- dispatch on the encryptionAlgorithm
802
857
  // OID. PBES2 uses the UTF-8 password (the pinned interop convention); legacy PBE uses the App. B.1 BMPString.
803
858
  function _decryptBag(ea, ct, password, opts, budget) {
804
- if (ea.oid === O("pbes2")) return pbes2.pbes2Decrypt(_pbePassword(password), ea.parameters, ct, opts, _err, "pkcs12");
859
+ // BOTH arms charge the one shared budget. Charging only the legacy arm left the modern one --
860
+ // the arm every current producer emits -- able to reset its per-bag cap on every bag, so a store
861
+ // that repeats a costly PBES2 bag up to the parser's element limit multiplied the cap by that
862
+ // limit in blocking pbkdf2Sync work.
863
+ if (ea.oid === O("pbes2")) {
864
+ var pw = _pbePasswordOwned(password);
865
+ try { return pbes2.pbes2Decrypt(pw.bytes, ea.parameters, ct, opts, _err, "pkcs12", budget); }
866
+ finally { _wipePw(pw); }
867
+ }
805
868
  return _decryptLegacyPbe(ea, ct, password, opts, budget);
806
869
  }
807
870
 
package/lib/pki-build.js CHANGED
@@ -223,15 +223,14 @@ function makeBuilder(ctx) {
223
223
  return nodeCrypto.createHash("sha1").update(keyBytes).digest();
224
224
  }
225
225
  function skiKeyId(val, spkiDer) {
226
- if (Buffer.isBuffer(val)) return val;
226
+ if (Buffer.isBuffer(val)) return guard.bytes.snapshot(val, ErrorClass, ctx.prefix + "/bad-input", "subjectKeyIdentifier");
227
227
  if (val === true) return spkiKeyId(spkiDer);
228
228
  throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a Buffer key id");
229
229
  }
230
230
 
231
231
  // ---- embedded-input validators ----
232
232
  function reqDer(v, what) {
233
- if (Buffer.isBuffer(v)) return v;
234
- if (v instanceof Uint8Array) return Buffer.from(v);
233
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, ErrorClass, ctx.prefix + "/bad-input", what);
235
234
  throw E("bad-input", what + " must be a DER Buffer");
236
235
  }
237
236
  // Full validation of an embedded SubjectPublicKeyInfo via the SAME parser the decoder uses.
package/lib/schema-cms.js CHANGED
@@ -47,6 +47,7 @@
47
47
  var asn1 = require("./asn1-der");
48
48
  var schema = require("./schema-engine");
49
49
  var pkix = require("./schema-pkix");
50
+ var guard = require("./guard-all");
50
51
  var oid = require("./oid");
51
52
  var frameworkError = require("./framework-error");
52
53
  var schemaX509 = require("./schema-x509");
@@ -211,6 +212,138 @@ function _checkContentBindingAttrs(attrs, mode) {
211
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)");
212
213
  }
213
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
+
214
347
  // RFC 5652 sec. 5.3 / sec. 9.3 -- when a content-type attribute is present, it MUST
215
348
  // be single-valued (sec. 11.1) and its value MUST equal the eContentType (a
216
349
  // cross-field consistency both parsed here). Shared by SignedData signedAttrs,
@@ -1150,7 +1283,15 @@ var CONTENT_INFO = schema.seq([
1150
1283
  * cms.signerInfos[0].sid.serialNumberHex; // -> "0a1b"
1151
1284
  * cms.encapContentInfo.eContent; // -> Buffer | null (detached)
1152
1285
  */
1153
- var parse = pkix.makeParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS });
1286
+ // Recording, for the reason the certificate and CRL parsers are: a SignedData is one or more
1287
+ // signatures over byte ranges, and a parsed one presents those ranges (`signedAttrsBytes`, the
1288
+ // encapsulated `eContent`), the signatures, and the certificates that verify them as separate
1289
+ // properties of one object. Keep a genuine signer's `signature` and `signedAttrsBytes` and replace
1290
+ // the `eContent` beside them, and every part of the check still passes for content that signer never
1291
+ // signed -- which is exactly the forgery pki.cms.verify's own block claims to defend. The verdict
1292
+ // verbs re-derive from what is recorded here, so the object a caller passes names bytes rather than
1293
+ // asserting facts.
1294
+ var parse = pkix.makeRecordingParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: CmsError, prefix: "cms", what: "CMS ContentInfo", topSchema: CONTENT_INFO, ns: NS }, "cms");
1154
1295
 
1155
1296
  /**
1156
1297
  * @primitive pki.schema.cms.pemDecode
@@ -1220,7 +1361,13 @@ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).
1220
1361
  // (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
1221
1362
  // that the strict `parse` entry would refuse). Same contract as
1222
1363
  // walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
1223
- function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
1364
+ // Records provenance, like parse: the structure a consumer hands to pki.cms.verify must be
1365
+ // re-derivable from the bytes it was walked from, and a PFX authSafe reaches verify this way. The
1366
+ // walker's own BER-tolerant decode is what replays it -- the strict `parse` entry would refuse the
1367
+ // indefinite-length encoding real stores carry.
1368
+ var walkSignedData = guard.parsed.recordingWalker("cms", function (node) {
1369
+ return schema.walk(SIGNED_DATA, node, NS).result;
1370
+ }, function (der) { return asn1.decode(der, { ber: true }); });
1224
1371
  function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
1225
1372
  // Validate + surface one countersignature value (RFC 5652 sec. 11.4, Countersignature ::=
1226
1373
  // SignerInfo) into the same parsed shape parse() gives a top-level SignerInfo -- so pki.cms.verify
@@ -1295,6 +1442,7 @@ module.exports = {
1295
1442
  walkSignedData: walkSignedData,
1296
1443
  walkEncryptedData: walkEncryptedData,
1297
1444
  walkCountersignature: walkCountersignature,
1445
+ looksLikeSignedAttributes: looksLikeSignedAttributes,
1298
1446
  assertAttachedCiphertext: assertAttachedCiphertext,
1299
1447
  // The structure's own algorithm tables, exported (like the walk* helpers) as the single source
1300
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/sigstore.js CHANGED
@@ -522,6 +522,9 @@ var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
522
522
  // The guard tests membership with hasOwnProperty, so the permitted set is a lookup object --
523
523
  // an array would treat "0"/"1" as the known keys and reject every real field name.
524
524
  var IDENTITY_KEYS = { san: 1, issuer: 1, sourceRepositoryURI: 1 };
525
+ // Every option pki.sigstore.verifyBundle reads. Adding one here is the only way to make it
526
+ // accepted, so a capability cannot arrive with its option silently ignored at this boundary.
527
+ var _VERIFY_BUNDLE_OPTS = { fulcioRoots: 1, rekorKeys: 1, identity: 1, predicateType: 1, time: 1 };
525
528
 
526
529
  // Returns WHICH fields were actually compared. A bundle verifies its own signature and log
527
530
  // inclusion whoever signed it -- Fulcio issues to anyone who completes an OIDC flow -- so
@@ -633,6 +636,13 @@ async function verifyBundle(bundle, opts) {
633
636
  throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
634
637
  }
635
638
  opts = opts || {};
639
+ // An unrecognized option is refused, not swallowed. The identity policy one level down already
640
+ // closes this door for exactly the reason it needs closing here too: cosign spells the signer pin
641
+ // `certificateIdentity`, and a swallowed spelling checks nothing under a name the operator
642
+ // believes pins the signer. At the top level the same slip loses the SLSA predicate pin as well,
643
+ // and `predicateType` has no `identityChecked`-style field, so nothing in the verdict reveals
644
+ // that the pin never ran.
645
+ guard.identifier.assertKnownKeys(opts, _VERIFY_BUNDLE_OPTS, _err, "sigstore/bad-input", "pki.sigstore.verifyBundle has an unknown option ");
636
646
  var b = parseBundle(bundle);
637
647
  var vm = b.verificationMaterial;
638
648
  var env = b.dsseEnvelope;