@blamejs/pki 0.5.17 → 0.5.19

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/jose.js CHANGED
@@ -42,6 +42,24 @@ var webcrypto = wcEngine.webcrypto;
42
42
  var frameworkError = require("./framework-error");
43
43
  var guard = require("./guard-all");
44
44
  var edwardsPoint = require("./edwards-point");
45
+ // What this module reads from the runtime while it is deciding something, taken at load. A JWS is
46
+ // signed and verified against a header the caller owns, and the reads below are the ones that decide
47
+ // what that header IS: whether a value is a list, whether a name is the caller's own, which
48
+ // conversion turns a claim into the string compared against a profile. A caller who receives control
49
+ // afterwards -- through an accessor on the header, or a `toJSON` while it is being serialized -- can
50
+ // replace any of them, and every check around them still runs and still passes. See guard-intrinsic.
51
+ var intrinsic = require("./guard-intrinsic");
52
+ var _isArray = intrinsic.isArray;
53
+ var _hasOwn = intrinsic.hasOwn;
54
+ var _keys = intrinsic.keys;
55
+ var _isBuffer = intrinsic.isBuffer;
56
+ var _bufferFrom = intrinsic.bufferFrom;
57
+ var _stringify = intrinsic.stringify;
58
+ var _ObjectProto = intrinsic.ObjectProto;
59
+ var _push = intrinsic.uncurry(Array.prototype.push);
60
+ var _join = intrinsic.join;
61
+ var _map = intrinsic.map;
62
+ var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
45
63
 
46
64
  var JoseError = frameworkError.JoseError;
47
65
  function E(code, message, cause) { return new JoseError(code, message, cause); }
@@ -65,11 +83,11 @@ var LIMITS = constants.LIMITS;
65
83
  * pki.jose.base64url.encode(Buffer.from([1, 2, 3])); // -> "AQID"
66
84
  */
67
85
  function b64uEncode(bytes) {
68
- if (!Buffer.isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
86
+ if (!_isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
69
87
  // Re-view through the byte guard: a detached-backed Buffer reads as zero-length,
70
88
  // so a bare .toString would silently encode "" -- fail closed instead.
71
89
  bytes = guard.bytes.view(bytes, JoseError, "jose/bad-input", "base64url.encode input");
72
- return bytes.toString("base64url");
90
+ return _bufToString(bytes, "base64url");
73
91
  }
74
92
 
75
93
  /**
@@ -182,19 +200,19 @@ var PROFILES = {
182
200
  // { alg, algRow, header }; throws a typed jose/* fault on any violation.
183
201
  function _checkHeader(header, profileName) {
184
202
  var profile = PROFILES[profileName];
185
- if (!profile) throw E("jose/bad-input", "unknown JWS profile " + JSON.stringify(profileName));
186
- if (!header || typeof header !== "object" || Array.isArray(header)) throw E("jose/bad-header", "the protected header must be a JSON object");
203
+ if (!profile) throw E("jose/bad-input", "unknown JWS profile " + _stringify(profileName));
204
+ if (!header || typeof header !== "object" || _isArray(header)) throw E("jose/bad-header", "the protected header must be a JSON object");
187
205
  // Every field is read as an OWN property. JSON.stringify (used by sign) omits
188
206
  // inherited members, so an alg/nonce/url living on the prototype (a polluted or
189
207
  // Object.create(...) header) would pass here yet serialize to a header MISSING
190
208
  // them -- so require own-ness, matching what the serialized JWS will actually carry.
191
- var own = function (k) { return Object.prototype.hasOwnProperty.call(header, k); };
209
+ var own = function (k) { return _hasOwn(header, k); };
192
210
  // b64 (RFC 7797) MUST NOT be present at all -- the unencoded-payload downgrade.
193
211
  if (own("b64")) throw E("jose/bad-jws", "the RFC 7797 b64 header parameter is forbidden");
194
212
  var alg = header.alg;
195
213
  if (!own("alg") || typeof alg !== "string") throw E("jose/bad-header", "the protected header must carry a string alg");
196
214
  var algRow = profile.algs[alg];
197
- if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " + JSON.stringify(alg) + " for this JWS profile");
215
+ if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " + _stringify(alg) + " for this JWS profile");
198
216
  if (profile.requireUrl && (!own("url") || typeof header.url !== "string" || header.url.length === 0)) throw E("jose/bad-header", "the protected header must carry a non-empty string url");
199
217
  // nonce discipline.
200
218
  var hasNonce = own("nonce");
@@ -219,20 +237,20 @@ function _checkHeader(header, profileName) {
219
237
  } else if (profile.keyId === "jwk") {
220
238
  if (!hasJwk || hasKid) throw E("jose/bad-header", "this JWS must carry an embedded jwk, not kid");
221
239
  }
222
- if (hasJwk && (!header.jwk || typeof header.jwk !== "object" || Array.isArray(header.jwk))) throw E("jose/bad-header", "jwk must be a JSON object");
240
+ if (hasJwk && (!header.jwk || typeof header.jwk !== "object" || _isArray(header.jwk))) throw E("jose/bad-header", "jwk must be a JSON object");
223
241
  // crit: protected-only, non-empty, no standard names, no duplicates, all understood.
224
- if (Object.prototype.hasOwnProperty.call(header, "crit")) {
242
+ if (_hasOwn(header, "crit")) {
225
243
  var crit = header.crit;
226
- if (!Array.isArray(crit) || crit.length === 0) throw E("jose/bad-crit", "crit must be a non-empty array");
244
+ if (!_isArray(crit) || crit.length === 0) throw E("jose/bad-crit", "crit must be a non-empty array");
227
245
  var seen = {};
228
246
  for (var c = 0; c < crit.length; c++) {
229
247
  var name = crit[c];
230
248
  if (typeof name !== "string") throw E("jose/bad-crit", "crit entries must be strings");
231
- if (Object.prototype.hasOwnProperty.call(seen, name)) throw E("jose/bad-crit", "duplicate crit entry " + JSON.stringify(name));
249
+ if (_hasOwn(seen, name)) throw E("jose/bad-crit", "duplicate crit entry " + _stringify(name));
232
250
  seen[name] = 1;
233
- if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " + JSON.stringify(name));
251
+ if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " + _stringify(name));
234
252
  // Anything else is an extension parameter this implementation does not process.
235
- throw E("jose/bad-crit", "unprocessed critical header parameter " + JSON.stringify(name));
253
+ throw E("jose/bad-crit", "unprocessed critical header parameter " + _stringify(name));
236
254
  }
237
255
  }
238
256
  return { alg: alg, algRow: algRow, header: header };
@@ -267,9 +285,9 @@ function _cryptoAlg(algRow, jwk, key) {
267
285
  // The key type the JWK MUST have for this alg (fail closed on confusion).
268
286
  function _assertKeyType(algRow, jwk) {
269
287
  if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
270
- if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + JSON.stringify(jwk.kty) + " does not match the algorithm");
288
+ if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + _stringify(jwk.kty) + " does not match the algorithm");
271
289
  if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
272
- if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + JSON.stringify(jwk.crv));
290
+ if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + _stringify(jwk.crv));
273
291
  // node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
274
292
  // forged signature; reject a non-full-order Edwards point through the shared gate, the same check
275
293
  // every EdDSA verify path in the toolkit applies.
@@ -278,7 +296,7 @@ function _assertKeyType(algRow, jwk) {
278
296
  }
279
297
  // An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
280
298
  // the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
281
- if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + JSON.stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
299
+ if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + _stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
282
300
  }
283
301
 
284
302
  // The exact signature byte length this alg + key must produce.
@@ -317,10 +335,10 @@ var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
317
335
  * pki.jose.assertPublicJwk({ kty: "EC", crv: "P-256", x: "...", y: "..." });
318
336
  */
319
337
  function assertPublicJwk(jwk) {
320
- if (!jwk || typeof jwk !== "object" || Array.isArray(jwk)) throw E("jose/bad-key", "a JWK object is required");
338
+ if (!jwk || typeof jwk !== "object" || _isArray(jwk)) throw E("jose/bad-key", "a JWK object is required");
321
339
  for (var i = 0; i < PRIVATE_JWK_MEMBERS.length; i++) {
322
- if (Object.prototype.hasOwnProperty.call(jwk, PRIVATE_JWK_MEMBERS[i])) {
323
- throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " + JSON.stringify(PRIVATE_JWK_MEMBERS[i]));
340
+ if (_hasOwn(jwk, PRIVATE_JWK_MEMBERS[i])) {
341
+ throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " + _stringify(PRIVATE_JWK_MEMBERS[i]));
324
342
  }
325
343
  }
326
344
  return jwk;
@@ -374,9 +392,9 @@ var _VERIFY_KEYS = { key: 1, profile: 1 };
374
392
  async function verify(jws, opts) {
375
393
  opts = opts || {};
376
394
  guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, E, "jose/bad-input", "unknown pki.jose.verify option ");
377
- if (!jws || typeof jws !== "object" || Array.isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
378
- if (Object.prototype.hasOwnProperty.call(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
379
- if (Object.prototype.hasOwnProperty.call(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
395
+ if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
396
+ if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
397
+ if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
380
398
  if (typeof jws.protected !== "string" || typeof jws.signature !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
381
399
  if (typeof jws.payload !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
382
400
  var header = parseJson(b64uDecode(jws.protected));
@@ -393,7 +411,7 @@ async function verify(jws, opts) {
393
411
  // matters most -- a `key` that came back null from a lookup would verify against whatever the
394
412
  // message carried, and the verdict would report the embedded key as though none was named.
395
413
  // `undefined` alone means "not supplied", so spreading an options object stays safe.
396
- if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || Array.isArray(opts.key))) {
414
+ if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
397
415
  throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
398
416
  }
399
417
  var jwk = header.jwk || opts.key;
@@ -419,7 +437,7 @@ async function verify(jws, opts) {
419
437
  var want = _expectedSigBytes(checked.algRow, jwk);
420
438
  if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
421
439
  // Signing input from the RECEIVED base64url strings verbatim (never a re-serialize).
422
- var signingInput = Buffer.from(jws.protected + "." + jws.payload, "ascii");
440
+ var signingInput = _bufferFrom(jws.protected + "." + jws.payload, "ascii");
423
441
  var key;
424
442
  try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
425
443
  catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
@@ -457,7 +475,7 @@ async function verify(jws, opts) {
457
475
  * payload: Buffer // the raw payload octets ("" for POST-as-GET)
458
476
  * key: CryptoKey // the private signing key
459
477
  * profile: string // default "acme-outer"
460
- * jwk: object // the public JWK, when the header embeds jwk
478
+ * jwk: object // the public JWK, in kid mode (a header that embeds jwk supplies its own)
461
479
  *
462
480
  * @example
463
481
  * var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
@@ -466,13 +484,40 @@ async function verify(jws, opts) {
466
484
  * nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
467
485
  * var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
468
486
  */
487
+ // The public JWK has two sources and the embedded one wins, so opts.jwk is read only in kid mode.
488
+ // Supplied alongside a header that embeds its own, it selected nothing: the signature parameters
489
+ // came from the header's key while the caller had named a different one.
469
490
  var _SIGN_KEYS = { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 };
491
+ var _SIGN_KEYS_EMBEDDED_JWK = { protected: 1, payload: 1, key: 1, profile: 1 };
470
492
  async function sign(opts) {
471
493
  opts = opts || {};
494
+ // The options are checked BEFORE the header is touched. Serializing it runs the caller's own code
495
+ // -- a toJSON, an accessor -- and that code can delete an option from the very bag being checked,
496
+ // so a misspelling reached the door already removed and the message went out signed.
472
497
  guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
473
- var header = opts.protected;
498
+ // The header is then serialized ONCE, and every read below comes from a snapshot of those exact
499
+ // bytes: the header validated, the header embedded in the JWS and the key confirmed against the
500
+ // produced signature are then all the same values. Read live, a caller getter answers each of
501
+ // those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
502
+ // the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
503
+ var headerJson;
504
+ try { headerJson = _stringify(opts.protected); }
505
+ catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
506
+ // Read back through the same bounded reader a received JWS goes through, so a header this side
507
+ // produces is held to the caps and the duplicate-member rule the other side enforces.
508
+ var headerBytes = headerJson === undefined ? null : _bufferFrom(headerJson, "utf8");
509
+ var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
510
+ // Which of the two key sources is in force is known only once the header has been read, so the
511
+ // narrower table runs here. It re-walks the bag, which also catches a name that code added.
512
+ var embedsJwk = !!(header && typeof header === "object" && header.jwk);
513
+ if (embedsJwk) {
514
+ guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
515
+ return "unknown pki.jose.sign option " + _stringify(k) +
516
+ (k === "jwk" ? "; the protected header already embeds the public JWK" : "");
517
+ });
518
+ }
474
519
  var payload = opts.payload;
475
- if (!Buffer.isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
520
+ if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
476
521
  // Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
477
522
  // and would be silently signed as an empty POST-as-GET body -- fail closed instead.
478
523
  payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
@@ -494,16 +539,16 @@ async function sign(opts) {
494
539
  // also PUBLISHED to the server, so it must be public-only: an accidentally exported
495
540
  // private JWK must never be serialized into the protected header.
496
541
  if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
497
- var protectedB64 = b64uEncode(Buffer.from(JSON.stringify(header), "utf8"));
542
+ var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
498
543
  var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
499
- var signingInput = Buffer.from(protectedB64 + "." + payloadB64, "ascii");
544
+ var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
500
545
  var jwk = header.jwk || opts.jwk || {};
501
546
  // opts.key is documented as a CryptoKey, so one created outside this engine signs here too: it
502
547
  // carries none of the material this engine signs with, and is re-imported through it rather than
503
548
  // reaching subtle.sign as a key it cannot read. A key whose material this process cannot reach at
504
549
  // all -- non-extractable, or held behind another implementation's interface -- is refused there.
505
550
  var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
506
- var sigBuf = Buffer.from(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
551
+ var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
507
552
  // Pin the produced signature length the same way verify does. A key whose curve /
508
553
  // hash / modulus does not match the header alg (a P-384 key under ES256, an
509
554
  // HS512 key under HS256) still signs, but emits the wrong length -- caught here
@@ -563,15 +608,17 @@ var THUMBPRINT_MEMBERS = {
563
608
  async function thumbprint(jwk) {
564
609
  if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
565
610
  var members = THUMBPRINT_MEMBERS[jwk.kty];
566
- if (!members) throw E("jose/bad-key", "unsupported JWK key type " + JSON.stringify(jwk.kty));
611
+ if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(jwk.kty));
567
612
  var parts = [];
568
613
  for (var i = 0; i < members.length; i++) {
569
614
  var m = members[i];
570
- if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + JSON.stringify(m));
571
- parts.push(JSON.stringify(m) + ":" + JSON.stringify(jwk[m]));
615
+ if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
616
+ // The canonical form IS the thumbprint preimage (RFC 7638 sec. 3.3), so the serializer that
617
+ // builds it is the captured one: a replacement changes the identity every consumer compares.
618
+ _push(parts, _stringify(m) + ":" + _stringify(jwk[m]));
572
619
  }
573
- var canonical = "{" + parts.join(",") + "}";
574
- var digest = Buffer.from(await webcrypto.subtle.digest("SHA-256", Buffer.from(canonical, "utf8")));
620
+ var canonical = "{" + _join(parts, ",") + "}";
621
+ var digest = _bufferFrom(await webcrypto.subtle.digest("SHA-256", _bufferFrom(canonical, "utf8")));
575
622
  return b64uEncode(digest);
576
623
  }
577
624
 
@@ -603,7 +650,7 @@ async function thumbprint(jwk) {
603
650
  * pss.map(function (r) { return r.alg; }); // -> ["PS256", "PS384", "PS512"]
604
651
  */
605
652
  function sigAlgs() {
606
- return Object.keys(SIG_ALGS).map(function (alg) {
653
+ return _map(_keys(SIG_ALGS), function (alg) {
607
654
  var row = SIG_ALGS[alg];
608
655
  var out = { alg: alg, kty: row.kty };
609
656
  if (row.crv) out.crv = row.crv;
package/lib/key.js CHANGED
@@ -305,6 +305,10 @@ async function import_(input, opts) {
305
305
  }
306
306
  var algorithm = opts.algorithm != null ? opts.algorithm : _inferAlgorithm(detected);
307
307
  var isPublic = detected.format === "spki";
308
+ // Checked at this verb's own door rather than left for the engine to discover: a non-list reached
309
+ // the CryptoKey copy, where `Array.prototype.slice` on a number yields an empty list, so the key
310
+ // was minted permitted for NOTHING instead of the call naming the argument that was wrong.
311
+ if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
308
312
  var usages = opts.usages || _importUsages(_algName(algorithm), isPublic);
309
313
  var extractable = opts.extractable != null ? opts.extractable : false;
310
314
  try {
@@ -344,6 +348,8 @@ async function generate(algorithm, opts) {
344
348
  guard.identifier.assertKnownKeys(opts, _GENERATE_OPTS, _err, "key/bad-input",
345
349
  "pki.key.generate has an unknown option (the WebCrypto spelling is `extractable`): ");
346
350
  var extractable = opts.extractable != null ? opts.extractable : true;
351
+ // The same door as import: a non-list is named here rather than becoming an empty usage set.
352
+ if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
347
353
  var usages = opts.usages || _generateUsages(_algName(algorithm));
348
354
  var pair;
349
355
  try { pair = await subtle.generateKey(algorithm, extractable, usages); }
@@ -43,6 +43,7 @@ var x509 = require("./schema-x509");
43
43
  var schemaCrl = require("./schema-crl");
44
44
  var key = require("./key");
45
45
  var cms = require("./cms-verify"); // pki.cms.sign (build the id-signedData authSafe) + pki.cms.verify (the open integrity gate) + cms.encrypt (the privacy envelope)
46
+ var cmsSign = require("./cms-sign"); // the signer-descriptor field tables, for the integrity signers this verb authors
46
47
  var cmsDecrypt = require("./cms-decrypt"); // the @internal decryptEnvelopedData seam -- opens a privacy safe off the pkcs12 parser's already-walked (BER-capable) EnvelopedData node
47
48
  var schemaPkcs12 = require("./schema-pkcs12");
48
49
  var pkix = require("./schema-pkix");
@@ -116,6 +117,91 @@ var _MISSING_PASSWORD = "a password must be a string, Buffer, or Uint8Array -- a
116
117
  // Every field opts.integrity reads. Adding one here is the only way to make it accepted, so a
117
118
  // capability cannot arrive with its option silently ignored at this boundary.
118
119
  var _INTEGRITY_OPTS = { mode: 1, signer: 1, signers: 1, certificates: 1, sid: 1, signingTime: 1 };
120
+ // The two caller-owned objects pki.pkcs12.build reads. The spec keys are what _normalizeSpec
121
+ // consumes -- the explicit `safeContents` form and the `{ key, cert, ca }` shorthand -- so the table
122
+ // is derived from that function rather than from the doc block. opts.integrity already had its own
123
+ // door; the objects containing it did not, so a misspelled `certificates` at the top level was
124
+ // dropped and the store built without them.
125
+ // The spec comes in two mutually exclusive FORMS, so it is checked against the one it selects.
126
+ // `_normalizeSpec` returns `spec.safeContents` the moment it is an array and never looks at the
127
+ // shorthand fields, so a union accepted `{ safeContents: [...], key }` and built a store with the
128
+ // key silently absent -- the same omission the door exists to refuse, reached by mixing the forms
129
+ // rather than by misspelling one.
130
+ var _BUILD_SPEC_FULL_KEYS = { safeContents: 1 };
131
+ var _BUILD_SPEC_SHORTHAND_KEYS = { key: 1, cert: 1, ca: 1, friendlyName: 1, localKeyId: 1 };
132
+ // `recipientCerts` selects the privacy recipients and is read by _normalizeSpec, which _build hands
133
+ // opts to -- it belongs here even though _build never names it. The three verbs this module exports
134
+ // have disjoint option sets (verifyMac reads maxIterations / recipientKey / allowUnauthenticated,
135
+ // open reads signerCerts / keys / importAlgorithm), so build gets its own table rather than a
136
+ // shared one that would admit theirs.
137
+ // Per spec FORM, because `recipientCerts` is read only by the shorthand branch of _normalizeSpec.
138
+ // Under the full form it is read by nothing: a caller who put a plaintext key bag in safeContents
139
+ // and supplied recipientCerts expecting public-key privacy got an id-data safe holding the private
140
+ // key in cleartext. The full form builds its own safes, so privacy there is `recipients` on the
141
+ // entry.
142
+ var _BUILD_OPTS_FULL = { integrity: 1, mac: 1, password: 1, pem: 1 };
143
+ var _BUILD_OPTS_SHORTHAND = { integrity: 1, mac: 1, password: 1, pem: 1, recipientCerts: 1 };
144
+ // The fields a safeContents entry carries. This entry is where the PRIVACY directive lives, which
145
+ // makes an unrecognized name here the costliest misspelling in the format: a misspelled `encrypt`
146
+ // is neither present nor falsy, so it passes the present-but-falsy guard in _buildAuthSafeElement,
147
+ // no privacy is selected, and the safe goes out as plaintext id-data -- an unshrouded keyBag in the
148
+ // clear, inside a PFX whose MAC still verifies and which opens without complaint.
149
+ // Per privacy branch, because the branches read different fields:
150
+ // `contentEncryptionAlgorithm` is the public-key branch's content cipher and is read by NOTHING on
151
+ // a password safe, whose cipher comes from `encrypt.cipher`. A union accepted an explicit cipher
152
+ // request on a password safe and encrypted under the default anyway.
153
+ var _SAFE_RECIPIENTS_KEYS = { bags: 1, recipients: 1, contentEncryptionAlgorithm: 1 };
154
+ var _SAFE_PASSWORD_KEYS = { bags: 1, encrypt: 1 };
155
+ var _SAFE_PLAINTEXT_KEYS = { bags: 1 };
156
+ // The fields a bag carries, PER TYPE rather than as one union. A union would admit a field that
157
+ // this bag's own arm never reads, which is worse here than a dropped attribute: `encrypt` on a
158
+ // `key` bag is read by nothing, so the private key is emitted as a plaintext keyBag while the
159
+ // caller supplied an explicit encryption directive and the outer MAC gives integrity only. Naming
160
+ // the fields per type makes that combination a refusal instead of a silent downgrade. Written out
161
+ // per type rather than merged at load, so each row reads as the whole contract for that bag.
162
+ var _BAG_KEYS_BY_TYPE = {
163
+ key: { type: 1, friendlyName: 1, localKeyId: 1, key: 1 },
164
+ shroudedKey: { type: 1, friendlyName: 1, localKeyId: 1, key: 1, encrypt: 1 },
165
+ cert: { type: 1, friendlyName: 1, localKeyId: 1, cert: 1 },
166
+ crl: { type: 1, friendlyName: 1, localKeyId: 1, crl: 1 },
167
+ secret: { type: 1, friendlyName: 1, localKeyId: 1, secretTypeId: 1, secretValue: 1 },
168
+ safeContents: { type: 1, friendlyName: 1, localKeyId: 1, nested: 1 },
169
+ };
170
+ // The PBE descriptor both privacy levels share (_pbeOpts is the one place it is read, so one door
171
+ // covers the safe-level and bag-level spellings). A misspelled parameter reverts to the built-in
172
+ // default, so a caller who asked for a stronger KDF or a different cipher silently gets neither.
173
+ var _ENCRYPT_KEYS = { password: 1, cipher: 1, salt: 1, iterations: 1, prf: 1 };
174
+ // The MAC descriptor. Every field has a default, so a misspelled one reverts to the shipped value:
175
+ // a caller who asked for a higher iteration count, a different hash or a longer key gets the
176
+ // default instead, and the store carries no record of which was used.
177
+ // Per MAC algorithm: the classic Appendix B key derivation produces a key at the hash's own output
178
+ // length and has no keyLength to set, so admitting one there took an explicit security parameter and
179
+ // emitted the same MAC as a call that never named it. Only PBMAC1 (RFC 9579) derives to a length.
180
+ var _MAC_KEYS = { algorithm: 1, hash: 1, salt: 1, iterations: 1, keyLength: 1, password: 1 };
181
+ var _MAC_KEYS_HMAC = { algorithm: 1, hash: 1, salt: 1, iterations: 1, password: 1 };
182
+ var _MAC_KEYS_PBMAC1 = { algorithm: 1, hash: 1, salt: 1, iterations: 1, keyLength: 1, password: 1 };
183
+ // The reading verbs take their own option sets, disjoint from build's. `maxIterations` is the one
184
+ // that matters most: it caps the work a hostile store can demand, so a misspelling restores the
185
+ // built-in ceiling and the caller's tighter bound is never applied.
186
+ // The table is what the CALL reads, including through its delegations -- and those cross a module
187
+ // boundary here. recipientKey / recipientCert are read by _openEncryptedSafe; `recipientIndex` is
188
+ // read by cms-decrypt's decryptEnvelopedData, which _openEncryptedSafe hands this whole options
189
+ // object to, so a table built from this file's own `opts.` reads alone would refuse a documented
190
+ // option and break selecting a recipient by index in a multi-recipient store.
191
+ //
192
+ // One table here, not one per branch, and the distinction is who selects the branch. A builder's
193
+ // options are checked against the form the CALLER chose, because a field the chosen form never
194
+ // reads is a request that silently did not happen. Reading a store, the branch is chosen by the
195
+ // BYTES: which integrity mode it carries, and whether a safe is password- or recipient-encrypted,
196
+ // are not known until it is parsed. These options are the policy a caller declares in advance for
197
+ // whatever the store turns out to be, so one that goes unread means the store did not need it --
198
+ // signerCerts on a MAC'd store, recipientKey on one with no enveloped safe. Refusing those would
199
+ // make a correct, store-agnostic call fail on the shape of its input.
200
+ var _OPEN_OPTS = {
201
+ allowUnauthenticated: 1, importAlgorithm: 1, keys: 1, maxIterations: 1, signerCerts: 1,
202
+ recipientKey: 1, recipientCert: 1, recipientIndex: 1,
203
+ };
204
+ var _VERIFY_MAC_OPTS = { maxIterations: 1 };
119
205
 
120
206
  // The same encoding, reporting ownership, mirroring pbes2.passwordBytesOwned. A caller-supplied
121
207
  // Buffer is returned as-is and is borrowed: clearing it would destroy the caller's own credential,
@@ -280,6 +366,14 @@ function _safeBag(bagName, bagValueDer, bag) {
280
366
 
281
367
  function _buildBag(bag, opts, depth) {
282
368
  if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
369
+ // Checked against THIS type's table. An unrecognized type falls through to the switch below,
370
+ // which is the one place that names the permitted set.
371
+ if (guard.intrinsic.hasOwn(_BAG_KEYS_BY_TYPE, bag.type)) {
372
+ guard.identifier.assertKnownKeys(bag, _BAG_KEYS_BY_TYPE[bag.type], _err, "pkcs12/bad-input", function (k) {
373
+ return "unknown field " + JSON.stringify(k) + " on a " + JSON.stringify(bag.type) +
374
+ " bag; that field is not read for this bag type";
375
+ });
376
+ }
283
377
  switch (bag.type) {
284
378
  case "key": {
285
379
  var keyDer = _coerceDer(bag.key, "keyBag key");
@@ -290,9 +384,12 @@ function _buildBag(bag, opts, depth) {
290
384
  var kDer = _coerceDer(bag.key, "shroudedKey key");
291
385
  try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
292
386
  var enc = bag.encrypt || {};
387
+ // The descriptor is checked BEFORE its password is read, so no field of an object carrying an
388
+ // unrecognized name reaches the KDF -- the same order the safe-level path uses below.
389
+ var bagPbe = _pbeOpts(enc);
293
390
  var pw = _pbePasswordOwned(enc.password != null ? enc.password : opts.password);
294
391
  var r;
295
- try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, _pbeOpts(enc), _err, "pkcs12"); }
392
+ try { r = pbes2.pbes2Encrypt(pw.bytes, kDer, bagPbe, _err, "pkcs12"); }
296
393
  finally { _wipePw(pw); }
297
394
  return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
298
395
  }
@@ -349,6 +446,10 @@ function _reqDer(input, label) {
349
446
  }
350
447
 
351
448
  function _pbeOpts(enc) {
449
+ guard.identifier.assertKnownKeys(enc, _ENCRYPT_KEYS, _err, "pkcs12/bad-input", function (k) {
450
+ return "unknown encrypt field " + JSON.stringify(k) +
451
+ "; the PBE descriptor is { password?, cipher?, salt?, iterations?, prf? }";
452
+ });
352
453
  var cipher = CIPHER_NAME[enc.cipher || "aes-256-cbc"];
353
454
  if (!cipher) throw _err("pkcs12/bad-input", "unsupported PBES2 cipher " + JSON.stringify(enc.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
354
455
  return {
@@ -379,6 +480,18 @@ async function _buildAuthSafeElement(sc, opts) {
379
480
  var hasRecipients = sc.recipients !== undefined;
380
481
  var hasEncrypt = sc.encrypt !== undefined;
381
482
  if (hasEncrypt && hasRecipients) throw _err("pkcs12/bad-input", "a safeContents cannot combine encrypt (password) and recipients (public-key) -- one ContentInfo is one privacy type (RFC 7292 sec. 4.1)");
483
+ // Checked against the branch the directive above selects, and BEFORE that branch reads anything.
484
+ // The branches read different fields: `contentEncryptionAlgorithm` is the public-key branch's
485
+ // cipher and is read by nothing on a password safe, whose cipher comes from `encrypt.cipher` --
486
+ // so a union accepted an explicit cipher request on a password safe and encrypted under the
487
+ // default anyway.
488
+ guard.identifier.assertKnownKeys(sc, hasRecipients ? _SAFE_RECIPIENTS_KEYS : (hasEncrypt ? _SAFE_PASSWORD_KEYS : _SAFE_PLAINTEXT_KEYS),
489
+ _err, "pkcs12/bad-input", function (k) {
490
+ return "unknown safeContents field " + JSON.stringify(k) + " for a " +
491
+ (hasRecipients ? "public-key safe; it takes { bags, recipients, contentEncryptionAlgorithm? }"
492
+ : hasEncrypt ? "password safe; it takes { bags, encrypt } and its cipher comes from encrypt.cipher"
493
+ : "plaintext safe; it takes { bags }, and a privacy directive must be encrypt or recipients");
494
+ });
382
495
  var safeContentsDer = _buildSafeContents(sc.bags || [], opts, 0);
383
496
  if (hasRecipients) {
384
497
  if (!Array.isArray(sc.recipients) || !sc.recipients.length) throw _err("pkcs12/bad-input", "safeContents.recipients must be a non-empty array of recipient descriptors (RFC 5652 sec. 6.1)");
@@ -401,9 +514,12 @@ async function _buildAuthSafeElement(sc, opts) {
401
514
  return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
402
515
  }
403
516
  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");
517
+ // Checked before the password is read, so no field of a descriptor carrying an unrecognized name
518
+ // reaches the KDF.
519
+ var safePbe = _pbeOpts(sc.encrypt);
404
520
  var pw = _pbePasswordOwned(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
405
521
  var r;
406
- try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12"); }
522
+ try { r = pbes2.pbes2Encrypt(pw.bytes, safeContentsDer, safePbe, _err, "pkcs12"); }
407
523
  finally { _wipePw(pw); }
408
524
  var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
409
525
  var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
@@ -464,11 +580,26 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
464
580
  // Accept the OpenSSL-style convenience form ({ key, cert, ca, friendlyName, localKeyId }) or the full
465
581
  // inverse-of-parse form ({ safeContents: SafeContentsSpec[] }). The convenience form -> one privacy safe of
466
582
  // PBES2 cert bags + one shrouded-key safe.
467
- function _normalizeSpec(spec, opts) {
468
- if (spec && Array.isArray(spec.safeContents)) return spec.safeContents;
583
+ // Which of the two forms a spec is gets decided ONCE, and that one decision is what both the door
584
+ // and this normalizer act on. Decided separately they can disagree: two spellings of "is this the
585
+ // full form" drift apart on their own, and a caller getter returning an array to one and something
586
+ // else to the other makes them disagree on demand -- the full form checked while the shorthand form
587
+ // is the one assembled.
588
+ function _specForm(spec) {
589
+ var isObject = !!spec && typeof spec === "object" && !Buffer.isBuffer(spec);
590
+ var safeContents = isObject ? spec.safeContents : null;
591
+ var full = guard.intrinsic.isArray(safeContents);
592
+ return { isObject: isObject, full: full, safeContents: full ? safeContents : null };
593
+ }
594
+
595
+ function _normalizeSpec(spec, opts, form) {
596
+ if (form.full) return form.safeContents;
469
597
  if (spec && (spec.key != null || spec.cert != null)) {
470
- var enveloped = opts.recipientCerts != null;
471
- if (enveloped && (!Array.isArray(opts.recipientCerts) || !opts.recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
598
+ // One read of the caller's array, so the emptiness check and the recipients built from it cannot
599
+ // be handed different values.
600
+ var recipientCerts = opts.recipientCerts;
601
+ var enveloped = recipientCerts != null;
602
+ if (enveloped && (!guard.intrinsic.isArray(recipientCerts) || !recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
472
603
  var certBags = [];
473
604
  if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
474
605
  if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
@@ -480,7 +611,7 @@ function _normalizeSpec(spec, opts) {
480
611
  // caller password, if any, remains the MAC (integrity) password; privacy and integrity are independent.
481
612
  var bags = certBags.slice();
482
613
  if (spec.key != null) bags.push({ type: "key", key: spec.key, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
483
- sc.push({ recipients: opts.recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
614
+ sc.push({ recipients: recipientCerts.map(function (c) { return { cert: c }; }), bags: bags });
484
615
  return sc;
485
616
  }
486
617
  if (certBags.length) sc.push({ encrypt: { password: opts.password }, bags: certBags });
@@ -512,7 +643,7 @@ function _normalizeSpec(spec, opts) {
512
643
  *
513
644
  * @opts
514
645
  * - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
515
- * - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, keyLength? }`.
646
+ * - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations? }`. `keyLength` is a `pbmac1` field: the classic Appendix B derivation produces a key at the hash's own output length, and naming one under `hmac` is refused.
516
647
  * - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected.
517
648
  * - `recipientCerts` -- (public-key privacy convenience) `[certDer|PEM, ...]`; the convenience-form cert + key are placed in one recipient-enveloped safe (a plaintext keyBag). For the full form, set per-`safeContents` `recipients: [{ cert }, ...]` -- CERTIFICATE recipients only (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM, dispatched off the cert key); a password or KEK recipient is not public-key privacy and is rejected -- with an optional `contentEncryptionAlgorithm` (`aes-128|192|256-cbc`, default 256; GCM/AEAD rejected). `encrypt` (password) and `recipients` (public-key) on the same safe is rejected; privacy is independent of integrity.
518
649
  * - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
@@ -541,6 +672,26 @@ function build(spec, opts) {
541
672
 
542
673
  async function _build(spec, opts) {
543
674
  opts = opts || {};
675
+ // Before the spec is normalized, so a misspelled bag field is a refusal rather than a store built
676
+ // without it. A PKCS#12 file that silently omits the certificate or the key a caller asked for
677
+ // looks well-formed and opens cleanly; the omission surfaces wherever it is later imported.
678
+ var form = _specForm(spec);
679
+ if (form.isObject) {
680
+ guard.identifier.assertKnownKeys(spec, form.full ? _BUILD_SPEC_FULL_KEYS : _BUILD_SPEC_SHORTHAND_KEYS,
681
+ _err, "pkcs12/bad-input", function (k) {
682
+ return "unknown PKCS#12 spec field " + JSON.stringify(k) + " for the " +
683
+ (form.full ? "safeContents form; the shorthand fields belong to { key, cert, ca } and are not read here"
684
+ : "shorthand form; pass safeContents instead to build the safes yourself");
685
+ });
686
+ }
687
+ // The options answer to the same form: recipientCerts is read only by the shorthand branch.
688
+ guard.identifier.assertKnownKeys(opts, form.full ? _BUILD_OPTS_FULL : _BUILD_OPTS_SHORTHAND, _err, "pkcs12/bad-input",
689
+ function (k) {
690
+ return "pki.pkcs12.build has an unknown option " + JSON.stringify(k) +
691
+ (form.full && k === "recipientCerts"
692
+ ? " for the safeContents form; the full form carries privacy per entry, as safeContents[i].recipients"
693
+ : "");
694
+ });
544
695
  // The integrity mode is checked against the permitted set rather than compared to one literal.
545
696
  // Compared, any other spelling reads as "not public-key" and silently selects password integrity,
546
697
  // dropping the signer with it: a caller who wrote "publicKey" gets a MAC where they asked for a
@@ -556,7 +707,7 @@ async function _build(spec, opts) {
556
707
  // RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
557
708
  // config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
558
709
  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)");
559
- var safeContentsSpecs = _normalizeSpec(spec, opts);
710
+ var safeContentsSpecs = _normalizeSpec(spec, opts, form);
560
711
  if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
561
712
  var elements = [];
562
713
  for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(await _buildAuthSafeElement(safeContentsSpecs[i], opts));
@@ -569,13 +720,48 @@ async function _build(spec, opts) {
569
720
  // password) provides the integrity, and there is NO MacData. Compose pki.cms.sign -- so every cms.sign
570
721
  // signer algorithm (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) ships for free.
571
722
  var ig = opts.integrity;
723
+ // `signer` is the one-signer spelling of `signers`, and the list wins outright, so supplying
724
+ // both signed the store under the list alone and dropped the single signer without a word.
725
+ if (ig.signers != null && ig.signer != null) throw _err("pkcs12/bad-input", "supply exactly one of opts.integrity.signer or opts.integrity.signers, not both");
572
726
  var signers = ig.signers != null ? ig.signers : (ig.signer != null ? [ig.signer] : null);
573
727
  if (!Array.isArray(signers) || !signers.length) throw _err("pkcs12/bad-input", "public-key integrity requires opts.integrity.signer or opts.integrity.signers (a cms.sign signer descriptor)");
728
+ // The signer descriptors are authoring input for this store, so they answer to the same rule as
729
+ // every other field the caller writes here, against the form each one selects. Every field
730
+ // beyond the identity has a default: without the door a misspelled `pss` or `digestAlgorithm`
731
+ // signed the store under PKCS#1 with SHA-256 while the caller had asked for something else, and
732
+ // the store records only what was used, never what was requested.
733
+ for (var sx = 0; sx < signers.length; sx++) {
734
+ var sd = signers[sx];
735
+ if (sd == null || typeof sd !== "object" || Buffer.isBuffer(sd)) throw _err("pkcs12/bad-input", "each opts.integrity signer must be a descriptor object");
736
+ var keyOnly = sd.cert == null && sd.spki != null;
737
+ guard.identifier.assertKnownKeys(sd, keyOnly ? cmsSign.KNOWN_SIGNER_KEY_ONLY_KEYS : cmsSign.KNOWN_SIGNER_CERT_KEYS,
738
+ _err, "pkcs12/bad-input", function (k) {
739
+ return "unknown opts.integrity signer field " + JSON.stringify(k) + " for the " + (keyOnly
740
+ ? "key-only form; it takes { key, spki, keyIdentifier } plus the signature parameters"
741
+ : "certificate form; it takes { key, cert } plus the signature parameters");
742
+ });
743
+ }
574
744
  var authSafeCi = await cms.sign(authSafeDer, signers, { eContentType: "data", detached: false, certificates: ig.certificates !== false, sid: ig.sid, signingTime: ig.signingTime });
575
745
  pfx = b.sequence([b.integer(3n), b.raw(authSafeCi)]); // PFX ::= SEQUENCE { version v3, authSafe (id-signedData) } -- NO MacData
576
746
  } else {
577
747
  var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
578
- if (opts.mac != null && opts.mac !== false && typeof opts.mac !== "object") throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
748
+ // An array passes `typeof x === "object"`, and its own names are indices no table lists, so an
749
+ // array reached _buildMacData as an empty descriptor and produced the DEFAULT MAC. Excluded by
750
+ // the same test opts.integrity already applies, read through the load-time capture so a
751
+ // replaced Array.isArray cannot reopen it.
752
+ if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" || guard.intrinsic.isArray(opts.mac))) throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
753
+ if (opts.mac != null && opts.mac !== false) {
754
+ // The algorithm selects the derivation, so the table that runs is the one belonging to it.
755
+ // An unrecognized spelling keeps the union; _buildMacData refuses it by name a moment later.
756
+ var macAlg = opts.mac.algorithm == null ? "hmac" : opts.mac.algorithm;
757
+ var macKeys = macAlg === "hmac" ? _MAC_KEYS_HMAC : macAlg === "pbmac1" ? _MAC_KEYS_PBMAC1 : _MAC_KEYS;
758
+ guard.identifier.assertKnownKeys(opts.mac, macKeys, _err, "pkcs12/bad-input", function (k) {
759
+ return "opts.mac has an unknown option " + JSON.stringify(k) +
760
+ (macAlg === "hmac" && k === "keyLength"
761
+ ? " for the classic HMAC algorithm; its key is derived at the hash's own output length, and only pbmac1 takes a keyLength"
762
+ : "");
763
+ });
764
+ }
579
765
  if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
580
766
  pfx = b.sequence(pfxChildren);
581
767
  }
@@ -612,6 +798,9 @@ async function _build(spec, opts) {
612
798
  * var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
613
799
  */
614
800
  async function verifyMac(pfx, password, opts) {
801
+ if (opts != null && typeof opts === "object" && !Buffer.isBuffer(opts)) {
802
+ guard.identifier.assertKnownKeys(opts, _VERIFY_MAC_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.verifyMac has an unknown option ");
803
+ }
615
804
  return _verifyMacOfStore(_storeFromBytes(pfx), password, opts);
616
805
  }
617
806
 
@@ -715,7 +904,8 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
715
904
  */
716
905
  async function open(pfx, password, opts) {
717
906
  opts = opts || {};
718
- if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
907
+ guard.identifier.assertKnownKeys(opts, _OPEN_OPTS, _err, "pkcs12/bad-input", "pki.pkcs12.open has an unknown option ");
908
+ if (opts.maxIterations != null &&(typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
719
909
  throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
720
910
  }
721
911
  var m = _storeFromBytes(pfx);